API¶
Returns Portal 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.
Returns Portal 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 three transitions, for the system that keeps the
merchant's records: poll the request collection on filter_date_modified_from,
walk the cursor, and settle what your own operator decided.
Codes of its own¶
Beside the envelope's, this version answers with codes for conditions in the merchant's own domain:
| Code | Status |
|---|---|
transition_not_allowed |
409 |
request¶
A customer's request to send lines of an order back, as the portal recorded it.
index.php?route=extension/returns_portal/api/v1/request
| One record | &request_id= |
| The collection | the same route without &request_id, ordered date_added DESC, request_id DESC |
| Verbs | GET, and POST on each transition below |
Fields¶
In the order they are emitted in.
| Field | Type | Notes |
|---|---|---|
request_id |
integer | |
rma |
string | The code the customer quotes. Its format is not promised: a client must not parse the id out of it. |
store_id |
integer | |
opencart_order_id |
integer | |
opencart_customer_id |
integer | 0 for a guest, and for an erased request. |
firstname |
string | Empty on an erased request. |
lastname |
string | Empty on an erased request. |
email |
string | Empty on an erased request. For a guest it is the only identity the request has. |
telephone |
string | Empty on an erased request. |
resolution |
string | One of refund, credit, exchange. |
status |
string | One of submitted, approved, rejected, cancelled, closed. |
currency_code |
string | ISO 4217 alpha-3, snapshotted off the order. |
currency_value |
string | The order's frozen rate, 8 decimal places. |
estimate_suppressed |
boolean | The estimate is arithmetic over an order the extension knows it cannot fully account for, and was withheld from the customer. |
date_ordered |
string, YYYY-MM-DD | null |
|
date_erased |
string, RFC 3339 | null | Non-null means the empty contact fields above are deliberate. |
date_added |
string, RFC 3339 | |
date_modified |
string, RFC 3339 | |
lines |
array of line |
|
credit |
object | null | The credit shape. Null where no store credit has been claimed for the request. |
refunds |
array of refund |
What the merchant says they paid outside the store. The extension moved no money. |
history |
array of history |
The single fetch only, and absent from a collection entry. A transition answers with it, so the caller sees the row its own call wrote. |
The collection¶
The order is the contract, not a default. It is date_added DESC, request_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_status |
string | One of submitted, approved, rejected, cancelled, closed. Comma-separated. A draft is not a state a request can be listed in. |
filter_resolution |
string | One of refund, credit, exchange. Comma-separated. |
filter_store_id |
integer | 0 is the default store. Unfiltered is every store, which is what the credential authorises. |
filter_opencart_order_id |
integer | |
filter_opencart_customer_id |
integer | 0 lists the requests filed by guests. |
filter_date_added_from |
string, RFC 3339 | Inclusive. A date alone means midnight, store-local. |
filter_date_added_to |
string, RFC 3339 | Inclusive. |
filter_date_modified_from |
string, RFC 3339 | Inclusive. The incremental-synchronisation filter. |
filter_date_modified_to |
string, RFC 3339 | Inclusive. |
Writes¶
Each is a POST to a named sub-address of the resource, with a JSON body and
the record itself in data on success. A retry that arrives after the first
call landed is refused rather than performed twice, and the client's move is to
re-fetch the record.
request.decide¶
Settle every line of a submitted request, or refuse the rest of an approved one. The status is derived from the verdicts and is never supplied.
| Body field | Type | Notes | |
|---|---|---|---|
refused |
array of integer |
Required | The line_ids this decision refuses. [] accepts every line. A line already refused stays refused whatever this call says. |
comment |
string | Optional | At most 2000 characters. Becomes the history row's comment, which is where a rejection reason lives. The customer may read it. |
Beside the envelope's codes, it may answer transition_not_allowed (409).
request.close¶
The parcel arrived and the request is settled: approved to closed. May debit the merchant where their own automatic-credit setting fires on this arrow, and may change stock where their restock setting is on.
| Body field | Type | Notes | |
|---|---|---|---|
comment |
string | Optional | At most 2000 characters. Becomes the history row's comment. |
not_restocked |
array of integer |
Optional | The line_ids not to put back in stock. Every accepted, stock-tracked line is put back when the merchant's restock setting is on. Ignored when it is off. An id that is not an accepted line of this request is ignored. |
Beside the envelope's codes, it may answer transition_not_allowed (409).
request.cancel¶
The customer withdrew it: submitted to cancelled, and from nowhere else. The lines are left undecided, because nobody decided them.
| Body field | Type | Notes | |
|---|---|---|---|
comment |
string | Optional | At most 2000 characters. Becomes the history row's comment. |
Beside the envelope's codes, it may answer transition_not_allowed (409).
attachment¶
A photo the customer attached to a line of their request, as metadata. The bytes
are on its content sub-address.
index.php?route=extension/returns_portal/api/v1/attachment
| One record | &attachment_id= |
| The collection | the same route without &attachment_id, ordered date_added DESC, attachment_id DESC |
| Verbs | GET |
Fields¶
In the order they are emitted in.
| Field | Type | Notes |
|---|---|---|
attachment_id |
integer | |
line_id |
integer | null | Null on a pending upload nothing has claimed. Never null on a photo embedded on a line. |
filename |
string | The stored name, its extension derived from the verified type. Not an address: the file lives outside the document root. |
mime |
string | Verified from the bytes, never what the browser declared. |
bytes |
integer | Measured on disk. The content route sends no Content-Length, so this is the byte count. |
label |
string | The customer's own filename. Customer input. |
date_added |
string, RFC 3339 |
The collection¶
The order is the contract, not a default. It is date_added DESC,
attachment_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_line_id |
integer | The line the photo hangs off. A pending upload has none, so it is only ever in the unfiltered collection. |
Representations¶
A sub-address answering the bytes themselves rather than the envelope. A body that does not parse as JSON is what these routes promise, and only these.
attachment.content¶
The photograph itself, as the bytes the customer uploaded.
index.php?route=…/attachment.content&attachment_id=
Content-Type is the record's own mime. A refusal is still the envelope: the
bytes are the success case and nothing else.
Embedded shapes¶
What a field of type array or object holds. Each is declared once and
referred to from wherever it is embedded, so two copies cannot come to
disagree.
line¶
One line of the order coming back, as the customer selected it and the merchant settled it.
| Field | Type | Notes |
|---|---|---|
line_id |
integer | |
opencart_order_product_id |
integer | |
opencart_product_id |
integer | |
product |
string | Snapshotted off the order, never read back from the catalogue. |
model |
string | Snapshotted off the order. |
quantity |
integer | Units coming back, not units ordered. |
opened |
boolean | |
opencart_return_reason_id |
integer | Core's oc_return_reason, which a merchant may rename or delete. |
verdict |
string | One of pending, approved, rejected. pending only on a submitted request: a decision settles every line, so a decided request holds no pending line. |
estimate |
object | The estimate shape. |
opencart_return_id |
integer | null | Null means not yet mirrored. It is a retryable state and never an error. |
attachments |
array of attachment |
Metadata only. The bytes are on the attachment resource. |
date_added |
string, RFC 3339 | |
restock |
object | null | The restock shape. Null where closing the request put nothing of this line back in stock. |
estimate¶
What one line is worth, in the request's own currency.
| Field | Type | Notes |
|---|---|---|
base |
money object | The line's share of the order's product total. |
discount |
money object | Allocated off the order's own totals, and never recomputed from the catalogue. |
tax |
money object | |
total |
money object | What the line is worth. Check estimate_suppressed on the request before posting it into a ledger. |
attachment¶
A photo the customer attached, as metadata.
| Field | Type | Notes |
|---|---|---|
attachment_id |
integer | |
line_id |
integer | null | Null on a pending upload nothing has claimed. Never null on a photo embedded on a line. |
filename |
string | The stored name, its extension derived from the verified type. Not an address: the file lives outside the document root. |
mime |
string | Verified from the bytes, never what the browser declared. |
bytes |
integer | Measured on disk. The content route sends no Content-Length, so this is the byte count. |
label |
string | The customer's own filename. Customer input. |
date_added |
string, RFC 3339 |
credit¶
The store credit claimed for the request, and whether the money moved.
| Field | Type | Notes |
|---|---|---|
amount_estimated |
money object | What the snapshot said the request was worth. |
amount_issued |
money object | What the merchant actually issued, which they may have edited. |
opencart_customer_transaction_id |
integer | 0 means the money did not move: somebody claimed the credit and the core transaction did not land. Retryable by the merchant, never by the API. |
actor_type |
string | One of customer, merchant, system, api. |
actor_id |
integer | |
date_added |
string, RFC 3339 |
restock¶
What closing the request put back in stock for one line, and whether it is on the shelf now.
| Field | Type | Notes |
|---|---|---|
quantity |
integer | Units put back, which is the line's returned quantity. |
in_effect |
boolean | false while the order's status is one OpenCart has already put the whole order back for, so the units are not counted twice. |
date_added |
string, RFC 3339 | When the close put it back. |
refund¶
A refund the merchant recorded against the request, as they wrote it down.
| Field | Type | Notes |
|---|---|---|
refund_id |
integer | |
amount |
money object | |
currency_code |
string | ISO 4217 alpha-3: the request's own currency, the one its estimate is shown in. |
date_refunded |
string, YYYY-MM-DD |
The day the merchant says they paid it, in the store's calendar. |
method |
string | Merchant prose. |
actor_type |
string | One of customer, merchant, system, api. |
actor_id |
integer | |
date_added |
string, RFC 3339 | When it was written down, which is not when it was paid. |
history¶
One recorded transition of the request.
| Field | Type | Notes |
|---|---|---|
status_from |
string | "" on the creation row, which is an arrival from nowhere rather than a transition out of draft. |
status_to |
string | One of submitted, approved, rejected, cancelled, closed. |
actor_type |
string | One of customer, merchant, system, api. |
actor_id |
integer | For api, core's own api_id. |
notify |
boolean | Whether the customer was actually told, which is a different question from who should have been mailed. |
comment |
string | Merchant prose. Carries the rejection reason. |
date_added |
string, RFC 3339 |
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 contact fields are returned in full, 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. Redacting a name here would remove no access from anybody and would break guest returns outright, since for a guest the email address is the only identity the request has. - A
request.closemay debit the merchant. Where their own Approve automatically and store-credit settings fire on that arrow, closing issues credit to the customer's balance. It is the merchant's setting doing what it says, and it is why the API needs no credit parameter — but a call you make can move their money. - A
request.closemay change stock. Where the merchant's restock setting is on, closing puts accepted lines back on the shelf, and the stock follows the order's status afterwards. - Erasure cannot reach a copy you have already pulled. An erased request
reads back with empty contact fields,
opencart_customer_idof0, no attachments and a non-nulldate_erased, and everything commercial about it unchanged. A synchronisation into your own system has made a second controlled copy of that personal data, outside anything this extension or this store can erase. That obligation is yours. - Nothing records that you read anything. There is no read log, no
per-credential audit trail and no rate limiting. A write leaves a history row
naming the actor
api; a read leaves nothing at all, on either side.