The API promise¶
Every extension that owns data speaks an HTTP API, and this page is the commitment attached to it: what will not change, what may, how long a version keeps answering after it has been replaced, and how you find out.
It is written for the person wiring a store into an ERP, a WMS or a 3PL: the one who will still be running this integration in three years and cannot ship a fix the same afternoon. Everything here is deliberate. Where a limit exists because of how OpenCart itself works, that is said rather than implied.
Anyone can put JSON on an endpoint. This page exists because almost nobody writes down what happens to that endpoint afterwards.
Which extensions answer, and on what version¶
| Extension | API version | Answering since | Reference |
|---|---|---|---|
| Abandoned Cart Recovery | v1 |
24 September 2026 | API reference |
| B2B Pricing | v1 |
22 September 2026 | API reference |
| Back In Stock | v1 |
7 September 2026 | API reference |
| Delivery Date | v1 |
2 October 2026 | API reference |
| Gift Cards | v1 |
2 October 2026 | API reference |
| Loyalty | v1 |
2 October 2026 | API reference |
| Pre-Order | v1 |
2 October 2026 | API reference |
| Product Bundles | v1 |
2 October 2026 | API reference |
| Product Feed | v1 |
13 September 2026 | API reference |
| Product Search | v1 |
2 October 2026 | API reference |
| Profitability Copilot | v1 |
19 September 2026 | API reference |
| Returns Portal | v1 |
4 September 2026 | API reference |
| Review Requests | v1 |
8 September 2026 | API reference |
| Wishlist | v1 |
9 September 2026 | API reference |
An extension is on this list once it has shipped an API in a release, and not before. An extension of ours that is not on it does not answer an API at all. There is no unlisted, undocumented or preview endpoint to ask about. Several extensions hold no data of their own and will never appear here.
Answering since is the release date of the first version of the extension to carry that API version. The twenty-four-month clock does not run from it. That clock starts the day a replacement ships, and until one does there is no removal date to publish. See How long a version keeps working.
The version is in the URL, and it belongs to the extension¶
An endpoint looks like this:
https://your-store.example/index.php?route=extension/returns_portal/api/v1/request
The v1 is Returns Portal's API version. It is not the extension's release
version and it is not shared with the other extensions. Returns Portal can go
from 1.4 to 2.0 without v1 moving, and a store can serve Returns Portal's v1
and Wishlist's v2 at the same time. Each extension versions its own API,
because each extension ships as its own archive on its own schedule.
Read the version segment as a capability probe, not as a warning. The API runs on your own server, so a client meets whatever version that particular store has installed. Asking which versions a store answers on is how you find out what it can do.
There is no version-less form of the address. Leaving the segment out is a
404. A short URL that silently follows whichever version we happen to call
current would give away the only thing the segment is for.
What your client has to do¶
Two things, and every promise below depends on them.
- Ignore fields you do not recognise. New fields appear in responses without a new version. A client that fails on an unexpected key has opted out of the promise.
- Tolerate a value you do not recognise, exactly as you tolerate a field you do not recognise. Where a field or a parameter takes one of a named list (a status, a resolution), that list may grow. A returns request may one day have a sixth status, and a client that treats an unknown one as a crash rather than as something I do not handle has opted out in the same way. The reverse holds for values you send us: a parameter that accepts three values may come to accept four, and never fewer.
What will not change while a version answers, and what may¶
Both lists below are generated from the policy the build enforces, so they cannot drift from it: the classifier and this page read one mapping, and amending it without republishing the page fails the documentation check. The first column is the name the build prints when it refuses a release, so the sentence you read here is the sentence a maintainer is shown there.
Nothing is promised by implication. If a change is not on the second list, it is breaking.
What the build refuses¶
These are breaking. A change on this list means a new version segment — never an edit to a version already serving — and the release that attempts it inside a serving version fails, naming the clause below.
| The build calls it | The clause it keeps |
|---|---|
version_removed |
A version stays reachable for at least twenty-four months after its successor is released. |
resource_removed |
A promised route keeps answering for the life of its version. |
route_changed |
A promised route keeps its address for the life of its version. |
resource_id_changed |
A resource keeps the field its single fetch is addressed by. |
method_removed |
A route keeps answering every verb it promised. |
transition_removed |
A promised transition keeps answering for the life of its version. |
representation_removed |
A promised representation keeps answering for the life of its version. |
representation_content_type_changed |
A representation keeps answering the content type it promised. |
field_removed |
A promised field keeps its name for the life of its version. |
field_type_changed |
A promised field keeps its JSON type for the life of its version. |
field_became_nullable |
A field that has never been null does not start being null. The declared type is unchanged and the harm is not: a null arriving where a client has never seen one is what the promise exists to prevent. |
field_became_single_fetch_only |
A field promised on a collection keeps arriving on the collection. |
response_enum_member_removed |
A member a response has emitted is not withdrawn or repurposed. |
request_enum_member_removed |
Tightening validation is breaking, always: a call that was accepted stays accepted. |
parameter_added_required |
Tightening validation is breaking, always: a call that was accepted stays accepted. |
parameter_removed |
An unrecognised parameter is a 400, so removing a parameter turns a working call into a rejection. That is tightening, and tightening is always breaking. |
parameter_became_required |
Tightening validation is breaking, always: a call that was accepted stays accepted. |
parameter_type_changed |
An accepted parameter keeps the type it accepts. |
parameter_default_changed |
A documented default is part of what a call that omits the parameter answers. |
parameter_bound_tightened |
Tightening validation is breaking, always: a call that was accepted stays accepted. |
error_code_removed |
A promised error code keeps its meaning and keeps being emitted. A condition that stops erroring is a loosening, and a code that vanished while its condition moved under another code is the exact break this clause names — the diff cannot tell the two apart, so it denies both. |
error_status_changed |
An error code keeps the HTTP status it is paired with. |
sort_order_changed |
A collection keeps its documented sort order, because that order is what the keyed cursor is keyed on. |
pagination_mode_changed |
A collection keeps paginating the way it promised to. |
What may change in any release¶
These are additive: a client obeying the two obligations above survives every one of them, which is what makes the list above worth anything.
| The build calls it | The clause it keeps |
|---|---|
version_added |
A new version is a new route tree beside the old one, and no client is on it yet. |
version_removed_after_window |
A version may be removed once the removal date its snapshot records has passed. |
version_removed_before_release |
A version that has never been released was never promised, so it may be withdrawn. |
resource_added |
A new resource is a new route, and no client is calling it yet. |
method_added |
A verb a route did not answer is a verb no client was using. |
transition_added |
A new transition is a new sub-address, and no client is posting to it yet. |
representation_added |
A new representation is a new sub-address, and no client is fetching it yet. |
field_added |
A client must ignore a field it does not recognise, so a new field breaks nothing. |
field_became_non_null |
A field that stops being null answers a case every client already handles. |
field_became_always_present |
A field promised only on the single fetch may start arriving on the collection too. |
response_enum_member_added |
A response enum is open: a client must tolerate a member it does not recognise exactly as it tolerates a field it does not recognise. |
request_enum_member_added |
A request enum gaining a member accepts a call that used to be refused, which is a loosening. |
parameter_added_optional |
A parameter a caller may omit changes nothing for a caller who omits it. |
parameter_became_optional |
A parameter a caller may now omit changes nothing for a caller who still sends it. |
parameter_bound_loosened |
A bound that admits more than it did admits every call it admitted before. |
error_code_added |
A code for a condition that had none tells a client something it could not previously be told. |
Four promises the build cannot check¶
They are promised exactly as firmly as the rows above, and held by review rather than by a diff. Saying which is fairer than implying the machine catches everything. See how the promise is kept for why each is out of the build's reach.
- A field still means what it meant. The name and the type are checked; what the field counts is not.
- An error code still means what it meant. Your code branches on the code, and the branch keeps working.
- A credential that authenticates today still authenticates. We never tighten what we require of a caller within a version.
- The envelope does not change: the fixed outer shape of every response, successful or failed.
Free to change, and on neither list¶
- The human-readable message. It is prose, it is translated, and it is for a person reading a log. Never parse it.
- The order of keys within a JSON object. The order of items in a collection may not: every collection's sort order is documented, and the documented order holds.
- Response times. There is no performance promise here at all.
What counts as a breaking change¶
A breaking change means a new version segment, never an edit to a version already serving.
Tightening validation is breaking, always. If your working integration starts being rejected for input we used to accept, you have suffered exactly the harm this page exists to prevent, and "the old behaviour was sloppy" is not a defence we will offer. The single exception is where the loose behaviour was a security defect, and where that happens we say so plainly, as an exception, rather than folding it into a changelog line.
A bug fix does not bump the version, unless the fix is itself backwards-incompatible, in which case it is a breaking change wearing a bug fix's clothes and gets treated as one. Fixes are not rolled back into superseded versions, with the same exception for security.
How long a version keeps working¶
At least twenty-four months after its replacement ships.
The clock starts the day the next version is released, not the day we decide to
retire the old one. When v2 ships, v1's removal-eligible date is fixed and
published on the same day: v2's release date plus twenty-four months.
Read the wording carefully, because it is chosen. A version may be removed after its date. It is not promised to be. This is a floor on how long we support it, never a ceiling, and we expect to leave versions running well past their dates because deleting them buys us nothing.
Two things follow from this:
- We cannot push. If you never update the extension, your store keeps serving whatever version its files contain, for as long as you leave them there. The window binds merchants who upgrade; it does not migrate anybody. Do not assume a store has moved on because a date has passed. Ask it.
- The date is per extension, per version. Each extension publishes its own table on its Limits and guarantees page.
How you find out¶
Three places, two of which a machine can read.
- Every response from a superseded version carries a
Sunsetheader (RFC 8594) with that version's removal-eligible date, and aLinkheader withrel="sunset"pointing here. If you log response headers anywhere, you will know before anybody has to tell you. - The extension's changelog names the replacement in the release that ships it.
- The dated table on the extension's Limits and guarantees page, with one row per version, so you can read your own deadline off a page rather than reconstruct it from release notes.
How the promise is kept, and not just written¶
This promise is checked by the build.
Each extension declares its API contract as data: its routes, its response fields and their types, its error codes and the statuses they carry, its parameters and their defaults, and each collection's sort order. That declaration is snapshotted and committed. Every build compares the code against the snapshot and sorts each difference into the two lists above. An additive change updates the snapshot and passes. A breaking change fails the build, naming the clause it broke, and the only way forward is a new version beside the old one. The old snapshot and the old code stay where they are.
The installation test goes one step further and exercises every declared route against a real OpenCart store, on every OpenCart release the extension claims, so a contract that no longer matches what is actually served fails here rather than in your integration.
Four clauses on this page are held by review rather than by the build, and saying which is fairer than implying the machine catches everything. That a field still means what it meant, and that an error code still means what it meant, are judgements no diff can make. The build sees the name, the type and the status, and a field that changed what it counts would pass. That a credential which authenticates today still authenticates is proved by the installation test rather than by the snapshot. And the envelope is shared by every extension, so it is checked once, across all of them, rather than per contract.
What this page does not promise¶
- Availability and performance. This is PHP on your own server. What it does under load is a question about your hosting.
- The values, as opposed to the fields. We promise a field exists and what type it is. What OpenCart decides to store in it is OpenCart's business, and core changes between releases.
- Any scoping of access. A credential is not limited to one extension, one resource or one store. Any valid credential reads everything the installed extensions expose, across the whole installation. Treat one as access to all of it. See the extension's Limits and guarantees page for the full statement.
- An audit trail. We record nothing about who called what. Your web server's access log is the only record.
- Rate limiting. We impose none. Your host may impose whatever it likes, and that is between you and them.
- Webhooks. Nothing is pushed to you. Adding outbound calls later is not a breaking change and will not move a version segment.
- Anything undocumented. Behaviour that works today but is not in the published contract is not promised and may change without notice. This is the clause that makes every other clause on this page keepable, and it is only fair because the contract is complete and published. If you are relying on something, check that it is written down.
- Anything outside our code. Your reverse proxy, your security module, your PHP version and your host can each break an integration in ways nothing on this page reaches.