API¶
Pre-Order answers a JSON API over HTTPS, for the system that keeps the merchant's own records rather than for a browser.
This page is generated from the extension's contract declaration — the same data the routes are served from, the committed snapshot is diffed against and the release's smoke pass walks. So it cannot describe a route the version you installed does not answer, and a field that moves moves this page or fails the build.
A caller authenticates as one of OpenCart's own API users, under
System > Users > API, and the API has to be switched on for the
extension. That switch is under Extensions > Extensions > Modules, on
this extension's own settings screen, in its API section. With it off,
every route here answers 404 api_disabled, so an extension with its API off
is shaped like one that never had it.
That switch is independent of the extension's own status.
Pre-Order can be switched off while its API keeps answering, and that is
deliberate: a merchant who switches the extension off has stopped it doing
its job in the storefront, which is precisely when the system holding their
records still needs to read what is already there. Coupling the two would
break a nightly sync every time the merchant closed a window, with
404 api_disabled as the only clue.
Every answer is the same envelope¶
A success carries data. A collection carries page beside it:
{"data": [], "page": {"limit": 50, "count": 0, "cursor": null}}
A refusal carries error, and no data at all:
{"error": {"code": "not_found", "message": "No such record.", "status": 404}}
Exactly one of the two is present, always — including for a refusal the store made before it read anything of the call.
limit defaults to 50 and is clamped to 200; page.limit is the value
actually applied, so a caller who asked for more can see they were clamped
rather than infer it from a short page. page.cursor is null on the last page,
and it is the only reliable end-of-walk signal: a full page is not evidence of
more rows and a short one is not evidence of none. There is no total.
A money value is an object carrying amount, a string at four decimal places,
and the currency it is in. A day is YYYY-MM-DD; a moment is RFC 3339 with an
offset, and which offset is the last section on this page.
Branch on code. The message is prose for a person reading a log and is not
promised; status in the body is the status on the wire. A refusal that names
the parameters it rejected carries fields, a map of parameter name to the
codes that rejected it.
These codes belong to the envelope and mean the same thing in every one of our extensions that serves an API:
| Code | Status |
|---|---|
api_disabled |
404 |
tls_required |
426 |
unauthorized |
401 |
unknown_version |
404 |
unknown_route |
404 |
method_not_allowed |
405 |
not_found |
404 |
invalid_parameter |
400 |
malformed_json |
400 |
unsupported_media_type |
415 |
payload_too_large |
413 |
internal_error |
500 |
v1¶
Two read resources and no writes: line, one pre-ordered order line and its
derived state, and payment, the request that asks a customer to pay once the
stock is in — and never the payment link itself.
line¶
One pre-ordered order line: the product, what the customer was told to expect, and where it stands.
index.php?route=extension/preorder/api/v1/line
| One record | &line_id= |
| The collection | the same route without &line_id, ordered line_id DESC |
| Verbs | GET |
Fields¶
In the order they are emitted in.
| Field | Type | Notes |
|---|---|---|
line_id |
integer | This extension's id for the line. It changes when the order is edited: core rebuilds an edited order's lines, and this extension rebuilds its own with them, so an edited order's lines read as gone and new. opencart_order_id is the stable handle. |
opencart_order_id |
integer | |
opencart_order_product_id |
integer | Core's order line. Core gives it a new id when the order is edited, too. |
opencart_product_id |
integer | |
store_id |
integer | The order's storefront, read off the order. |
state |
string | One of waiting, ready, lapsed, paid, expired. Derived, never read off the column. waiting is not yet in stock; ready is in stock and, where payment was deferred, waiting to be paid; lapsed is ready with a payment request whose window has closed — its link no longer works, and the order can still be asked to pay again; paid is paid for; expired is given up on. paid on a line taken in full at checkout means charged then, not that the stock has arrived. |
payment_mode |
string | One of defer, full. How the line was taken: defer asks for payment when it is ready, full charged at checkout. Frozen when the order was placed. |
expected_date |
string, YYYY-MM-DD | null |
The day the customer was told to expect it, and null where no date was announced. |
created_at |
string, RFC 3339 | When the line was pre-ordered. Kept when an edit rebuilds the line. |
ready_at |
string, RFC 3339 | null | |
paid_at |
string, RFC 3339 | null |
The collection¶
The order is the contract, not a default. It is line_id DESC, it is what
the cursor is keyed on, and there is no sort parameter: a second order is a
second permanent promise and a second index to keep.
Walk it with &cursor=, taking the value from the previous page's
page.cursor and changing nothing else. The cursor names a row rather than a
position, so a row inserted mid-walk cannot shift the page under you.
It may be narrowed by these, and by nothing else — any other parameter is
400 invalid_parameter naming itself in fields.
| Parameter | Type | Notes |
|---|---|---|
filter_opencart_order_id |
integer | One order's lines, which is the read that survives an edit. |
filter_opencart_product_id |
integer | Everybody waiting on one product: the question a buyer placing a purchase order asks. |
payment¶
One payment request: when it went out, whether the customer was reminded, and when the link stops working.
index.php?route=extension/preorder/api/v1/payment
| One record | &payment_id= |
| The collection | the same route without &payment_id, ordered payment_id DESC |
| Verbs | GET |
Fields¶
In the order they are emitted in.
| Field | Type | Notes |
|---|---|---|
payment_id |
integer | This extension's id for the request. Stable when the request is sent again. |
opencart_order_id |
integer | One request per order at most. |
store_id |
integer | The order's storefront, read off the order. |
state |
string | One of open, lapsed, paid, expired. Derived from the order's lines and the window. open is payable now; lapsed is past its window, its link dead, and can be sent again; paid is every line paid; expired is given up on. |
requested_at |
string, RFC 3339 | When the request was last sent. It moves to now when the merchant sends it again, which also restarts the window. |
due_at |
string, RFC 3339 | null | When the link stops working, worked out from requested_at and the store's days-to-pay; null where the store's window is 0, never. Changing that setting moves every open request's deadline with no write to any of them. |
reminder_sent |
boolean | Whether the one reminder at the window's midpoint has gone out. There is no date for it. |
The collection¶
The order is the contract, not a default. It is payment_id DESC, it is
what the cursor is keyed on, and there is no sort parameter: a second order is
a second permanent promise and a second index to keep.
Walk it with &cursor=, taking the value from the previous page's
page.cursor and changing nothing else. The cursor names a row rather than a
position, so a row inserted mid-walk cannot shift the page under you.
It may be narrowed by these, and by nothing else — any other parameter is
400 invalid_parameter naming itself in fields.
| Parameter | Type | Notes |
|---|---|---|
filter_opencart_order_id |
integer | |
filter_reminder_sent |
integer | 1 for the requests already reminded, 0 for the rest. |
What a client has to do¶
Three obligations, and a client that meets them keeps working across every non-breaking change this API is allowed to make:
- Ignore a field you do not recognise. A new field is added without notice and without a version segment, so a client that refuses an unexpected key breaks on a change that was promised to be safe.
- Tolerate an unrecognised member of an enum exactly as you tolerate an
unrecognised field. A new state, resolution, verdict or actor is the same
kind of additive change as a new field. Branch on the members you know and
route the rest to a default; never
switchwith an exhaustiveelsethat throws. - Treat a response that does not parse as this envelope as a transport failure, and retry it. A proxy's HTML error page, a truncated body, a maintenance splash: none of those came from this API, and reading them as data is how a client invents a state the store never reported.
Timestamps, and which store answered¶
Every timestamp is RFC 3339 with an offset, and the offset is the one of whichever store the hostname you polled resolved to — not UTC, and not the default store's. A multi-store installation polled on two hostnames answers the same moment with two different offsets. Parse to an instant and compare instants; never compare the strings, and never assume the offset is stable across stores or across a daylight-saving boundary.
What v1 owes you before you build on it¶
Facts about this surface that no field list states, and that a client author would otherwise find out from a support ticket:
- No payment link is readable, by any name. A link's token lets whoever
holds it pay for the order; it is not a field, not a filter and not a sort
key. Whether the link still works is
state— a token being present proves nothing, because an expired request keeps its token. - Both collections are full walks; there is nothing to ask for
incrementally. Cancelling a line, a request expiring, the reminder going out
and the link being used all write without a stamp, and
lapsedmoves with the clock. Walking the cursor to the end — the last page'scursorisnull— is the whole synchronisation mechanism, and it is resumable. - Editing an order rebuilds its lines under new ids. Core gives an edited
order's lines new ids, and this extension rebuilds its own with them, keeping
each line's
created_at. So an edited order reads as lines gone and new lines arrived.opencart_order_idis the handle that survives; reconcile by absence. - The states are worked out on every call.
lapsedis the payment window having closed, read off the clock and the store's days-to-pay; nothing stores it, and on a store whose scheduler never runs — every OpenCart 4.1.0.4 store among them — a line can sitlapsedindefinitely rather than becomingexpired. Changing days-to-pay moves every open request's deadline and state at once. - There is no money here. What a customer owes is on OpenCart's own order,
which
opencart_order_idnames; this extension stores no amount at all. - Erasure here is deletion. Erasing a customer's personal data deletes their lines and requests outright, and deleting an order takes its lines out of every answer. Neither leaves a tombstone; a full walk is the authoritative list.
- Nothing records that you read anything, and the credential cannot be
scoped. An OpenCart API user opens every extension's API and core's own
api/orderbesides, across every store in the installation. There is no read log, no per-credential audit trail and no rate limiting; v1 writes nothing at all.