8. Collection transactions — Read-only
8.1 Portal transaction APIs
Both roles may call these endpoints with a user PAT:
| Method and path | Purpose |
|---|---|
GET /portal/collections/transactions |
Paginated merchant transactions |
GET /portal/collections/transactions/{transaction_id} |
One transaction by internal numeric ID |
transaction_id is the internal numeric ID. Both endpoints require environment, exactly test or live; the server combines the authenticated merchant, stored is_production flag, and detail ID. A mismatch returns 404 NOT_FOUND. Legacy transaction_type and APN context labels do not override the stored environment flag. Missing/invalid environment returns 422 VALIDATION_ERROR. Positive canonical decimal IDs accept at most 20 digits; malformed or inaccessible IDs return 404. The detail Form Request validates the route ID and accepts only the environment query, rejecting caller-supplied ID/filter overrides.
Both endpoints require the signed-in Portal user to have merchant-portal-read permission.
| Query | Contract |
|---|---|
environment |
Required: test or live |
from, to |
Optional paired calendar ISO 8601 instants with seconds, Z or explicit ±HH:MM, and optional 1–6 fractional digits; from < to; creation-time interval [from, to) |
status |
Optional PAID, PENDING, or REJECTED |
merchant_transaction_id |
Optional exact match, maximum 100 characters |
query |
Optional reference substring search, maximum 100 characters |
currency |
Optional three-letter uppercase code in the existing currency catalogue |
payment_brand |
Optional exact stored brand value, maximum 100 characters |
page, per_page |
Common pagination limits |
Supplied filters combine using AND on the server. If dates are omitted, list all stored collection transactions in the environment, including archived records. Sort by created_at DESC, id DESC; page defaults to 1, page size to 25, with per_page 1–100. Empty or out-of-range pages return 200, empty data, and normal meta. Unexpected fields and invalid filters return 422 VALIDATION_ERROR; conflicting merchant selectors are rejected earlier with 403 MERCHANT_SCOPE_MISMATCH.
Dates require real calendar instants and offsets up to ±14:00. Offsetless/date-only values, impossible dates, unmatched bounds, and equal/reversed instants are rejected. Bounds are converted to config('app.timezone') (currently Asia/Singapore) to compare against the existing offsetless Laravel database timestamps. The start is inclusive and end exclusive; fractional bounds are preserved. This assumes existing stored timestamps follow that application timezone, which must be checked before deployment against imported/legacy data.
query searches merchant references; %, _ and ! are literal characters. Reference/brand/currency matching follows database collation. Unsupported currencies return 422; an unavailable catalogue returns 503 SERVICE_UNAVAILABLE for currency-filtered reads. Reads without the currency filter remain available.
Example request:
GET /api/v1/portal/collections/transactions?environment=test&from=2026-09-01T00%3A00%3A00Z&to=2026-10-01T00%3A00%3A00Z&status=PAID&page=1&per_page=25
Authorization: Bearer <user_pat>
Transaction data object:
{
"id": 1001,
"merchant_transaction_id": "ORDER202610010001",
"environment": "test",
"amount": "1500.00",
"currency": "PHP",
"status": "PAID",
"payment_brand": "Example brand",
"payment_method": null,
"created_at": "2026-10-01T08:00:00+08:00",
"updated_at": "2026-10-01T08:01:00+08:00",
"history_available": true
}
List uses the standard Portal success/message/data/meta envelope; detail uses success/message/data, message Success. Both project exactly the fields above and return Cache-Control: no-store, private. Amount is a two-decimal string via BCMath, or null when the legacy value is missing. Other missing legacy values/timestamps remain null. The current collection schema has no payment_method column; The API returns null rather than inferring it from a provider or gateway. Customer identifiers, raw callback/return URLs, provider credentials/context, payloads, fees, and arbitrary legacy reference data are excluded.
Status is normalized to PAID, PENDING or REJECTED. Recognized payment_status wins case-insensitively; otherwise legacy S means paid, R means rejected, and other values mean pending. Lists and reporting use the same status rules.
history_available is true when unpurged, permitted merchant ↔ DLT evidence exists for that transaction/environment. It does not guarantee readable bodies or complete history. Expired retention still counts until actual purge.
These GETs only read stored data. They do not write transaction/evidence records, create payments, inquire with providers, reconcile, or replay callbacks; no Portal mutation route is introduced. The existing API-wide 60/minute throttle applies. Both roles require an active merchant membership, authorized ecosystem PAT, and merchant-portal scope.
8.2 Request validation reference
These tables reflect the current implementation’s request rules. See input conventions for required/optional/null behavior and error formats.
Validation: GET /api/v1/portal/collections/transactions
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
page |
Query | Optional; default 1 | Integer, minimum 1. Null/empty values are invalid. |
per_page |
Query | Optional; default 25 | Integer, 1–100 inclusive. Null/empty values are invalid. |
environment |
Query | Required | String; exactly test or live, case-sensitive. |
from |
Query | Optional pair; required with to | String in YYYY-MM-DDTHH:mm:ss[.ffffff]Z or YYYY-MM-DDTHH:mm:ss[.ffffff]±HH:MM format; real calendar date/time, nonzero year, seconds required, optional 1–6 fractional digits, offset up to ±14:00. Date-only, offsetless and impossible instants are invalid. |
to |
Query | Optional pair; required with from | String in YYYY-MM-DDTHH:mm:ss[.ffffff]Z or YYYY-MM-DDTHH:mm:ss[.ffffff]±HH:MM format; real calendar date/time, nonzero year, seconds required, optional 1–6 fractional digits, offset up to ±14:00. Date-only, offsetless and impossible instants are invalid. Must be strictly later than from. |
status |
Query | Optional; required if supplied | String; exactly PAID, PENDING or REJECTED. |
merchant_transaction_id |
Query | Optional; required if supplied | Nonempty string, maximum 100 characters; exact reference match. |
query |
Query | Optional; required if supplied | Nonempty string, maximum 100 characters; reference substring search; %, _ and ! are literal characters. |
currency |
Query | Optional; required if supplied | String; exactly three uppercase ASCII letters and present in DLT’s currency catalogue. Unsupported code returns 422; unavailable catalogue returns 503. |
payment_brand |
Query | Optional; required if supplied | Nonempty string, maximum 100 characters; exact stored-brand match. |
Send no request body. Send both date bounds or neither; from < to; no reporting-specific ten-year or bucket limit is applied to this list. Filters combine with AND. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH.
Validation: GET /api/v1/portal/collections/transactions/{transaction}
| Field | Location | Requirement | Validation rules |
|---|---|---|---|
transaction |
Path | Required | Positive canonical decimal ID, 1–20 digits; pattern [1-9][0-9]{0,19}. No leading zero, sign or decimal point. Malformed, missing or inaccessible resources return 404 NOT_FOUND. |
environment |
Query | Required | String; exactly test or live, case-sensitive. |
Send no request body. Only environment is accepted as input outside the path. Do not resend route IDs or list filters in JSON/query. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH. The transaction must belong to this merchant and the selected environment.
8.3 Complete transaction read examples
The following are synthetic request/response pairs. Replace IDs and credential placeholders with values from your environment. GET requests have no body; filters belong in the query string. Examples show successful operations; validation and access rules above still apply.
GET /portal/collections/transactions
Request (no body):
GET /api/v1/portal/collections/transactions?environment=test&page=1&per_page=25
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Success",
"data": [
{
"id": 1001,
"merchant_transaction_id": "ORDER202610010001",
"environment": "test",
"amount": "1500.00",
"currency": "PHP",
"status": "PAID",
"payment_brand": "Example brand",
"payment_method": null,
"created_at": "2026-10-01T08:00:00+08:00",
"updated_at": "2026-10-01T08:01:00+08:00",
"history_available": true
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
}
}
GET /portal/collections/transactions/1001
Request (no body):
GET /api/v1/portal/collections/transactions/1001?environment=test
Accept: application/json
Authorization: Bearer <user_pat>
Response: 200.
{
"success": true,
"message": "Success",
"data": {
"id": 1001,
"merchant_transaction_id": "ORDER202610010001",
"environment": "test",
"amount": "1500.00",
"currency": "PHP",
"status": "PAID",
"payment_brand": "Example brand",
"payment_method": null,
"created_at": "2026-10-01T08:00:00+08:00",
"updated_at": "2026-10-01T08:01:00+08:00",
"history_available": true
}
}