API¶
Gift Cards 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.
Gift Cards 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: card, one of core's own gift vouchers with
what is left on it, the date it expires and the liability figure it is counted
in — and never its code.
card¶
One gift card: core's voucher, its balance, its expiry date and where the liability report counts it.
index.php?route=extension/gift_cards/api/v1/card
| One record | &opencart_voucher_id= |
| The collection | the same route without &opencart_voucher_id, ordered opencart_voucher_id DESC |
| Verbs | GET |
Fields¶
In the order they are emitted in.
| Field | Type | Notes |
|---|---|---|
opencart_voucher_id |
integer | Core's own id for the voucher, and the only identity a card has here. Not the code: the code is a bearer credential and is never emitted. |
opencart_order_id |
integer | null | The order that sold the card, and null for a card issued by hand from Sales > Gift Vouchers. Editing an order in core deletes the cards it sold and issues them again under new ids — see the disclosures. |
face_value |
money object | What the card was issued for, in the store's default currency: checkout converts a card bought in another currency before core stores it. |
remaining |
money object | What is left: the face value plus every redemption and refund core has recorded against the card, never below zero. Worked out on every call and stored nowhere. |
expires_on |
string, YYYY-MM-DD | null |
The last day the card can be redeemed, and null where it never expires — which includes every card that existed before Gift Cards was installed or before a validity period was set. Written once and never moved by a change to the store-wide period. |
expired |
boolean | Whether that day has passed, as at the moment you asked. An expired card is still owed: it is counted inside the headline figure, not taken out of it. |
switched_off |
boolean | Whether core's own Status on the voucher is off. Like expiry, it stops the card redeeming and does not forfeit what is on it. |
figure |
string | One of headline, hand_issued, order_incomplete, spent. Which of the liability report's figures the card is counted in. headline is money taken and goods not yet given; hand_issued and order_incomplete are shown beside it and never added into it, because no money was taken for either; spent has nothing left and is in no figure at all. Depends on the order statuses the store calls complete at the moment you ask, so a card can move between figures without anything about it changing. |
date_added |
string, RFC 3339 | When core created the voucher row. |
The collection¶
The order is the contract, not a default. It is opencart_voucher_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 | The cards one order sold. 0 is the cards issued by hand. |
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 card code is readable, by any name, through any route. A code is a live bearer credential — whoever holds it spends the balance — so it is not a field, not a filter and not a sort key, and an integration that needs to look a card up by its code cannot. Core's own voucher screen is where a code is read.
- The collection is a full walk; there is nothing to ask for incrementally.
Core's voucher row has no modified stamp, a redemption writes a history row
and touches nothing on the card, an expiry date is written without a stamp,
and
expiredandfiguremove with the clock and with the order's status. Walking the cursor to the end — the last page'scursorisnull— is the whole synchronisation mechanism, and it is resumable: a walk that stops resumes from the cursor it was last handed. - Every derived field is as at the moment you asked.
remaining,expired,switched_offandfigureare worked out on every call from core's own rows.figurealso depends on which order statuses the store calls complete, so changing that setting moves cards between figures with nothing about any card having changed. - A card disappears when core deletes it, and editing an order re-issues its cards. Core deletes the vouchers an order sold when the order is deleted, and when the order is edited it deletes them and creates them again at full value under new ids. So an edited order reads as cards gone and new cards arrived, and a redemption history recorded against the old id is not on the new one. Reconcile by absence: a full walk is the authoritative list.
- Nothing about who a card was for is readable. The sender, the recipient and the message are core's data on core's row and are not fields. Erasing a person therefore has nothing to reconcile here: no field this API emits is personal data.
- Every amount is in the store's default currency, and there is no store. A
voucher in core belongs to the installation rather than to a storefront, so
there is no
store_idand no store filter; the currency on every money field is the default store'sconfig_currency, which is the currency core stores a card's value in. - OpenCart 4.0.2.x only. OpenCart removed the voucher subsystem in 4.1.0.0, and Gift Cards refuses to install above 4.0.2.x, so this API answers on no later release.
- 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.