API¶
Delivery Date 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.
Delivery Date 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¶
One read resource and no writes: booking, the day and window one order was
promised, whether that promise still stands, and when any of it last changed.
booking¶
One order's delivery promise: the day, the window as the customer was told it, and its state, derived from the order.
index.php?route=extension/delivery_date/api/v1/booking
| One record | &opencart_order_id= |
| The collection | the same route without &opencart_order_id, ordered opencart_order_id DESC |
| Verbs | GET |
Fields¶
In the order they are emitted in.
| Field | Type | Notes |
|---|---|---|
opencart_order_id |
integer | Core's order id, and the booking's identity: one order has exactly one booking. |
store_id |
integer | The order's storefront, read off the order — the booking carries no store of its own. |
state |
string | One of scheduled, unscheduled, released. Derived, never stored. released is a booking whose order now carries one of the statuses the store releases a place on — cancelled, failed, refunded, as configured — whatever the booking row says. unscheduled is an order that could not be given a day. scheduled is everything else. |
delivery_date |
string, YYYY-MM-DD | null |
The day promised, and null for an unscheduled booking. |
slot_id |
integer | null | The time window's id, and null where the calendar offers no windows. It names configuration this API does not expose; slot_name is what the customer was told. |
slot_name |
string | The window as the customer was told it, frozen in the order's language when they chose it. A later rename of the window does not change it. Empty where there is no window. |
method_reference |
string | The shipping method the booking was made against, frozen the same way. |
chosen_date |
string, YYYY-MM-DD | null |
Set only when the booking moved at firming — the day the customer picked had filled up by the time payment confirmed — and then it is the day they picked. Null is the ordinary case. A merchant moving a delivery on the planner does not set it: that shows only in delivery_date and date_modified. |
chosen_slot_name |
string | The window the customer picked, beside chosen_date, and empty with it. |
date_firmed |
string, RFC 3339 | null | When the booking stopped being a hold and took its place, and null while it is still a hold. A hold occupies a place only for the store's hold window. |
date_added |
string, RFC 3339 | When the booking was first made, at the confirm step of checkout. |
date_modified |
string, RFC 3339 | null | When anything a caller can see last changed: the hold, the firming, a move on the planner, or the order gaining a history row — which is how a booking becomes released. Null on a booking that has not changed since the store updated to 1.6.0, because nothing recorded that moment before then. |
The collection¶
The order is the contract, not a default. It is opencart_order_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_store_id |
integer | 0 is the default store. |
filter_delivery_date_from |
string, YYYY-MM-DD |
Inclusive. With filter_delivery_date_to, the day sheet: every booking promised for a day. |
filter_delivery_date_to |
string, YYYY-MM-DD |
Inclusive. |
filter_date_modified_from |
string, RFC 3339 | Inclusive. The incremental read: every booking changed since a moment, a status change on its order included. A booking older than the store's update to 1.6.0 and untouched since is not in it — a first full walk is. |
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:
- The state is worked out on every call, from the order.
releasedis not stored anywhere: it is the order's current status being one the store releases a place on. Changing which statuses those are, under the extension's settings, moves bookings into or out ofreleasedwithout a write to any of them, and nothing stampsdate_modifiedwhen it happens. After changing that list, walk the collection again. - A booking appears when its order does, which is not when its id was given
out. A booking is made at the confirm step of checkout, against an order id
core allocated earlier, and an order still at status 0 — a checkout nobody
finished — is not answered at all. So a walk by id can pass a booking that
appears behind it;
filter_date_modified_fromis what finds it, because the order's first history row stamps it. date_modifiedmoves on the store's own writes, not on somebody else's. The checkout, the firming, the planner and every history row through core's own order model stamp it. A tool that writesoc_orderor this extension's table directly, around core, stamps nothing. A booking that existed before the store updated to 1.6.0 carriesnulluntil it next changes.- Erasure here is deletion. Erasing a customer's personal data deletes their bookings outright, and deleting an order takes its booking out of every answer. Neither leaves a tombstone, so a synchronisation that only adds rows keeps what the store destroyed. Reconcile by absence: a full walk is the authoritative list, and core's own order may still exist after its booking is gone.
- 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.