Skip to content
DLTdevelopers
Sign in
DLT Merchant Portal/Collections Public documentation

Transaction history

For DLT Merchant Portal developers · DLT maintainers · Contract v1.7

Configured API origin: https://stage-checkout.dxp.dtic.com.ph

Build with the right credentials. API access requires the documented authentication and permissions. Examples use synthetic data and placeholder origins.

9. Transaction history displayed in the Portal

The Portal reads sanitized history for the signed-in user's business. Here, merchant ↔ DLT describes past exchanges between that business's integration and DLT. These are records displayed by the Portal, not requests that the Portal should reproduce.

9.1 Portal history endpoints

Both roles use a user PAT. History is scoped to the authenticated merchant, internal transaction ID and required environment.

Method and path Query Result
GET /portal/collections/transactions/{transaction_id}/history Required environment=test|live; optional page>=1, per_page=1..100 (default 25) 200, chronological merchant ↔ DLT history with standard pagination and meta.coverage
GET /portal/collections/transactions/{transaction_id}/history/{history_id} Required environment=test|live 200, one permitted sanitized record

history_id is the stored 26-character uppercase ULID, not the internal evidence row ID. Transaction route IDs are canonical positive decimal strings, at most 20 digits. History is ordered by captured_at, then ULID, ascending. A known transaction without permitted captures returns data=[], including transactions unsupported by the existing capture service. A page beyond the last page is also empty. No event is reconstructed from the transaction's current state.

List and detail reject extra request fields, including attempts to override route IDs, select provider records, or request sensitive details. Detail rejects pagination fields. The existing merchant-context middleware returns 403 MERCHANT_SCOPE_MISMATCH for an incompatible supplied merchant identifier; other unexpected fields return 422 VALIDATION_ERROR. Invalid route ID shapes, unknown transactions/history IDs, cross-merchant/environment lookups, and history belonging to another transaction return the standard 404 NOT_FOUND without disclosing ownership. An invalid, expired or revoked user PAT returns 401; missing scope/inactive membership returns 403.

History reads are audited and use Cache-Control: no-store. Audit failure returns 500 instead of exposing history. There is no Portal reveal, replay, purge or provider-inquiry action.

9.2 Provenance and payload projection

History contains only the following merchant ↔ DLT events. Provider/internal exchanges are excluded; history_available counts unpurged permitted evidence.

Exchange Required source / direction / evidence type Portal treatment
Merchant → DLT validated request merchant_api / inbound / merchant_request.validated merchant_request; project request_body only; normalized validated capture rather than raw traffic
DLT → merchant synchronous response merchant_api / outbound / merchant_response.synchronous dlt_response; sanitized final formatted checkout response and HTTP/repeated-request metadata; supported synchronous capture
DLT → merchant queued webhook merchant_webhook / outbound / merchant_notification.queued merchant_webhook_request; project body and typed event metadata
Merchant → DLT webhook response merchant_webhook / inbound / merchant_notification.response merchant_webhook_response; each captured attempt has its own history item
Older outbound delivery attempt merchant_webhook / outbound / merchant_notification.delivery_attempt merchant_webhook_request, legacy_sanitized_snapshot; typed delivery metadata only, payload=null, capture_status=omitted

Legacy outbound attempts are not claimed as captured merchant replies. Provider request/response/notification, internal observations, unclassified snapshots, legacy core snapshots, and mismatched provenance are excluded in SQL. A permitted evidence type alone never permits the entire snapshot to leave DLT. authoritative_context, private PII, global integrity/previous hashes, and arbitrary historical omission manifests are not returned. Synchronous capture preserves only recognized fixed omission path/reason pairs from its capture policy. The Portal never selects or decrypts the private PII partition.

The projection supports evidence schema versions 1 and 2 and uses these fixed body fields:

Payload Allowed fields and types
Validated request merchant_transaction_id (up to 100 ASCII letters/digits/underscore/hyphen), payment_brand (same alphabet, up to 20), currency (three uppercase letters), amount (decimal string with two fractional digits), payment_status (PAID, PENDING, REJECTED if captured), channel (integer 1–4), is_card_payment (boolean if captured)
Queued webhook Same transaction/reference, brand, currency, amount, and payment-status fields if present; no channel/card flag
Merchant response JSON Boolean acknowledged, ok, success, accepted, received; status limited to ok, success, accepted, received, failed, error
Merchant response text Only exact acknowledgement text ok, ack, accepted, or received, after trimming and lowercase normalization
Legacy attempt No body; only typed event/delivery metadata from the recorded delivery object or legacy root

Absent fields are not filled from current transaction data. Amount input must be nonnegative, have at most 12 integer digits and at most two fractional digits; other shapes are excluded. Allowed identifiers are additionally screened by the existing sensitive-value guard. Free-form descriptions/messages, product/customer/contact/address data, payment/return/webhook URLs, response headers, credentials, signed JWTs, full card data, security codes, nested/unknown response fields, and arbitrary text are excluded. This conservative projection deliberately omits unsupported merchant response bodies instead of displaying free-form text that may contain personal data.

Safe event metadata includes event_id (same ASCII alphabet, up to 128; nullable), attempt_number (integer 1–1,000,000; nullable), http_status (integer 100–599; nullable), and optional delivery.state (pending, delivering, delivered, failed, exhausted) and delivery.duration_ms (integer 0–86,400,000). No delivery URL, error message, transport token, signature, or body digest is exposed. For merchant replies, delivery.body_capture_status preserves a recognized capture state or is null: no_response, empty, omitted_oversize, omitted_unsupported_content_type, omitted_binary, omitted_sensitive, captured, captured_sanitized, or captured_invalid_json_text. HTTP success and queued history do not prove business success or delivered payment status.

9.3 History response and availability

Example detail response (synthetic IDs; timestamps include offsets):

{
  "success": true,
  "message": "Success",
  "data": {
    "id": "01K6FA00000000000000000001",
    "transaction_id": 1001,
    "environment": "test",
    "event_type": "merchant_webhook_response",
    "direction": "merchant_to_dlt",
    "captured_at": "2026-10-01T16:01:02+08:00",
    "capture_format": "sanitized_snapshot",
    "capture_status": "available",
    "event_id": "evt_example",
    "attempt_number": 1,
    "http_status": 200,
    "delivery": { "body_capture_status": "captured" },
    "response": null,
    "payload": { "acknowledged": true },
    "omissions": [{ "field_path": "payload", "reason_code": "portal_field_allowlist" }],
    "retain_until": "2027-10-01T16:01:02+08:00",
    "purged_at": null
  }
}

A list has the same item shape under data, plus the standard pagination fields and coverage calculated across the entire scoped history, independently of the current page. This example has no synchronous response capture (for example, older records or a capture failure):

{
  "current_page": 1,
  "per_page": 25,
  "total": 3,
  "last_page": 1,
  "coverage": {
    "merchant_request": "captured",
    "dlt_response": "not_captured",
    "merchant_webhook_request": "captured",
    "merchant_webhook_response": "captured"
  }
}

Coverage is not_captured when no allowed record exists, purged when all records for that event type are purged, and captured when any unpurged record remains. It describes recorded events, not body availability or delivery success: an unreadable or omitted snapshot still counts as captured. not_applicable is reserved; History never infers it from missing history or current webhook configuration. Every captured attempt is an event, not another collection transaction.

capture_status Meaning
available The allowed body projection or known acknowledgement is available; other fields remain excluded
omitted Body was excluded at capture or projection, is empty, or is an unverified legacy body
unavailable Snapshot is missing/unreadable, schema unsupported, ciphertext exceeds the read bound, request body was not captured, or no merchant response was captured
purged Actual purged_at is set; payload is null and the encrypted snapshot is not decrypted

History never truncates a body; it omits unsupported fields/bodies as a whole. truncated remains a future capture-contract state and is not emitted here. Unsupported schemas or encrypted snapshots larger than 2 MiB are withheld before decryption. Corrupt ciphertext cannot break the remaining history page. event_id, attempt/HTTP metadata, delivery, and response metadata are null when absent, unsafe, or unavailable; purged records retain only public capture/retention metadata, not reconstructed event fields.

omissions contains server-generated value-free entries, never arbitrary historical field paths/reason strings. portal_field_allowlist identifies the applied projection and does not assert that every excluded field existed. Additional reasons distinguish legacy_body_unverified, snapshot_limit_exceeded, snapshot_schema_unsupported, snapshot_unreadable, snapshot_unavailable, body_not_captured, no_response, empty, the recognized omitted_* capture states, and response_fields_excluded (a present response whose permitted fields are empty). A sanitized snapshot is not byte-for-byte original traffic; omission metadata does not reconstruct discarded values.

Existing retention defaults to 12 months and supports legal holds. retain_until is the stored retention deadline, not a promise of immediate purge; an expired but unpurged record remains readable. GET never purges or extends retention. Actual purged records return payload=null and purged_at; private legal-hold reasons/actors are not exposed. Historical capture remains limited to the existing supported APN checkout/qualifying legacy flow. History is not backfilled.

9.4 Displaying recorded DLT responses

Synchronous dlt_response events capture responses prepared for the merchant on supported APN hosted checkout calls. Authentication/validation failures before transaction binding have no history. Repeated responses may create separate events without adding transactions.

The response body projection preserves captured values, without filling absent fields from current transaction data:

Field Permitted value
success Boolean, if present
error_code TRANSACTION_REFERENCE_CONFLICT, SUBMISSION_UNKNOWN, or SUBMISSION_IN_PROGRESS, if present
data.merchant_transaction_id Up to 100 ASCII letters/digits/underscore/hyphen, additionally screened by the sensitive-value guard
data.amount Original nonnegative decimal string, up to 12 integer digits and two fractional digits
data.payment_status PAID, PENDING, or REJECTED, if present
data.is_card_payment Boolean, if present
data.timestamp Captured timestamp with explicit offset or Z and a valid calendar date; optional 1–6 fractional digits

The stored capture and Portal projection both exclude usable payment_url, success_url and failure_url, returned provider transaction IDs, free-form message/errors, customer data, headers, credentials, signed tokens, card data, and unknown fields. The live checkout response remains unchanged; URLs are omitted from historical evidence, not removed from the response sent to the merchant. Capture stores only the approved projection and value-free omission metadata; it does not retain the full response in a private PII partition.

For a synchronous history item, delivery, event_id, and attempt_number are null. Its top-level http_status is DLT's returned status. response contains is_repeated_request (boolean), capture_stage=prepared_for_return, and body_capture_status. Other history event types have response=null; missing, unreadable, unsupported-schema, or purged synchronous snapshots also have response=null. A readable snapshot may retain safe response metadata while its body is unavailable. Metadata is revalidated when displayed.

{
  "id": "01K6FA00000000000000000002",
  "transaction_id": 1001,
  "environment": "test",
  "event_type": "dlt_response",
  "direction": "dlt_to_merchant",
  "captured_at": "2026-10-02T09:02:04+08:00",
  "capture_format": "sanitized_snapshot",
  "capture_status": "available",
  "event_id": null,
  "attempt_number": null,
  "http_status": 200,
  "delivery": null,
  "response": {
    "is_repeated_request": false,
    "capture_stage": "prepared_for_return",
    "body_capture_status": "captured_sanitized"
  },
  "payload": {
    "success": true,
    "data": {
      "merchant_transaction_id": "ORDER-001",
      "amount": "100.00",
      "payment_status": "PENDING",
      "is_card_payment": false,
      "timestamp": "2026-10-02T01:02:03Z"
    }
  },
  "omissions": [
    { "field_path": "payload", "reason_code": "portal_field_allowlist" },
    { "field_path": "response_body", "reason_code": "merchant_response_field_allowlist" },
    { "field_path": "response_body.data.payment_url", "reason_code": "payment_url_excluded" }
  ],
  "retain_until": "2027-10-02T09:02:04+08:00",
  "purged_at": null
}

Omission entries are fixed policy metadata, not a reconstruction of excluded values.

A JSON response is decoded only up to 256 KiB and a depth of 64. body_capture_status=captured_sanitized means permitted fields remain. omitted_oversize, omitted_non_json, omitted_invalid_json, omitted_unsupported_shape, and omitted_no_permitted_fields retain safe event/HTTP metadata with capture_status=omitted and payload=null. The existing 2 MiB encrypted-snapshot read bound, unavailable/corrupt/schema/purge handling, capture-time ordering, no-store reads, access audits, retention defaults and legal holds also apply. dlt_response coverage is captured when an unpurged synchronous event exists, purged when all such events are purged, and not_captured when none exists. Multiple response events do not increase collection transaction counts.

Capture failures preserve the payment response and can leave history gaps.

prepared_for_return does not prove merchant receipt or payment success. Old responses are never backfilled from current transaction state.

9.5 Provider coverage

Payload capture currently covers supported APN hosted checkout and qualifying legacy evidence. Other providers can appear in transactions and reporting while their history remains empty or incomplete. Missing evidence is not_captured; do not reconstruct it from transaction fields.

9.6 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/{transaction}/history

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.
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.

Send no request body. Only environment, page and per_page are accepted outside the path. 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/environment.

Validation: GET /api/v1/portal/collections/transactions/{transaction}/history/{history}

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.
history Path Required Uppercase canonical ULID: 26 characters, pattern [0-7][0-9A-HJKMNP-TV-Z]{25}; letters I, L, O and U excluded. Must be an allowed history item for this transaction/merchant/environment; otherwise 404.
environment Query Required String; exactly test or live, case-sensitive.

Send no request body. Only environment is accepted outside the path; pagination and caller-supplied route ID overrides are rejected. Unlisted payload/query fields are rejected with 422 VALIDATION_ERROR. A conflicting merchant selector may instead be rejected earlier with 403 MERCHANT_SCOPE_MISMATCH.

9.7 Complete history 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/1001/history

Request (no body):

GET /api/v1/portal/collections/transactions/1001/history?environment=test&page=1&per_page=25
Accept: application/json
Authorization: Bearer <user_pat>

Response: 200.

{
  "success": true,
  "message": "Success",
  "data": [
    {
      "id": "01K6FA00000000000000000002",
      "transaction_id": 1001,
      "environment": "test",
      "event_type": "dlt_response",
      "direction": "dlt_to_merchant",
      "captured_at": "2026-10-02T09:02:04+08:00",
      "capture_format": "sanitized_snapshot",
      "capture_status": "available",
      "event_id": null,
      "attempt_number": null,
      "http_status": 200,
      "delivery": null,
      "response": {
        "is_repeated_request": false,
        "capture_stage": "prepared_for_return",
        "body_capture_status": "captured_sanitized"
      },
      "payload": {
        "success": true,
        "data": {
          "merchant_transaction_id": "ORDER-001",
          "amount": "100.00",
          "payment_status": "PENDING",
          "is_card_payment": false,
          "timestamp": "2026-10-02T01:02:03Z"
        }
      },
      "omissions": [
        {
          "field_path": "payload",
          "reason_code": "portal_field_allowlist"
        },
        {
          "field_path": "response_body",
          "reason_code": "merchant_response_field_allowlist"
        },
        {
          "field_path": "response_body.data.payment_url",
          "reason_code": "payment_url_excluded"
        }
      ],
      "retain_until": "2027-10-02T09:02:04+08:00",
      "purged_at": null
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1,
    "coverage": {
      "merchant_request": "not_captured",
      "dlt_response": "captured",
      "merchant_webhook_request": "not_captured",
      "merchant_webhook_response": "not_captured"
    }
  }
}

GET /portal/collections/transactions/1001/history/01K6FA00000000000000000002

Request (no body):

GET /api/v1/portal/collections/transactions/1001/history/01K6FA00000000000000000002?environment=test
Accept: application/json
Authorization: Bearer <user_pat>

Response: 200.

{
  "success": true,
  "message": "Success",
  "data": {
    "id": "01K6FA00000000000000000002",
    "transaction_id": 1001,
    "environment": "test",
    "event_type": "dlt_response",
    "direction": "dlt_to_merchant",
    "captured_at": "2026-10-02T09:02:04+08:00",
    "capture_format": "sanitized_snapshot",
    "capture_status": "available",
    "event_id": null,
    "attempt_number": null,
    "http_status": 200,
    "delivery": null,
    "response": {
      "is_repeated_request": false,
      "capture_stage": "prepared_for_return",
      "body_capture_status": "captured_sanitized"
    },
    "payload": {
      "success": true,
      "data": {
        "merchant_transaction_id": "ORDER-001",
        "amount": "100.00",
        "payment_status": "PENDING",
        "is_card_payment": false,
        "timestamp": "2026-10-02T01:02:03Z"
      }
    },
    "omissions": [
      {
        "field_path": "payload",
        "reason_code": "portal_field_allowlist"
      },
      {
        "field_path": "response_body",
        "reason_code": "merchant_response_field_allowlist"
      },
      {
        "field_path": "response_body.data.payment_url",
        "reason_code": "payment_url_excluded"
      }
    ],
    "retain_until": "2027-10-02T09:02:04+08:00",
    "purged_at": null
  }
}
DLT Developer Documentation · Updated October 2, 2026