# 

Two endpoints. `GET /v1/transactions/{id}` reads one; `GET /v1/transactions` searches. Both return the same transaction object you got when you created it.

Because there are no webhooks, these are also how you learn about anything that happens after the response: settlement, and ACH returns.

## Read one transaction

```json 200 application/json
{
  "id": "TRANSACTION-01KEW32V6YNV11T33XGDR7TGWC",
  "reference_transaction_id": null,
  "type": "SALE",
  "result": "APPROVED",
  "client_id": "CLIENT-01KFDKXMQ637EKEAY410MSQSXB",
  "client_dba": "Acme Jewelry",
  "integrations": [
    "TSYS"
  ],
  "response_code": "00",
  "response_description": "Approved",
  "batch_id": "01KEWZ8R5MT3V9N2QXHDK7BPF4",
  "is_settled": true,
  "time_created": "2026-07-15T14:22:05Z",
  "amount": 10000,
  "tip": 0,
  "tax_amount": 500,
  "currency": "USD",
  "payment_method": {
    "type": "CARD",
    "truncated_pan": "****-****-****-5439",
    "payment_token": "CARD_TOKEN-01KFDKXMQ637EKEAY410MSQSXB",
    "expiry": {
      "month": "01",
      "year": "2028"
    },
    "first_name": "John",
    "last_name": "Doe",
    "entry_method": "keyed",
    "auth_code": "123456",
    "retrieval_reference_number": "000000603076",
    "card_type": "CREDIT",
    "card_brand": "Visa",
    "cvv_result_code": "M",
    "avs_result_code": "Y",
    "avs_response": "Exact Match - Street address and postal code match"
  },
  "recurring": false,
  "initiator": null,
  "network_transaction_id": "000000004271001",
  "metadata": {
    "order_id": "ORD-10432",
    "sales_channel": "web",
    "customer_reference": "cust-8891"
  }
}
```

```json 400 application/json
{
  "error_code": "INVALID_REQUEST_DATA",
  "error_message": "The API request contains invalid data.",
  "details": {
    "amount": "Invalid amount",
    "payment_method.pan": "Malformed PAN"
  },
  "source": {
    "system": "TSYS",
    "code": "13",
    "message": "Invalid amount"
  }
}
```

```json 401 application/json
{
  "error_code": "NOT_AUTHENTICATED",
  "error_message": "Access credentials missing or invalid.",
  "details": null,
  "source": {
    "system": null,
    "code": null,
    "message": null
  }
}
```

```json 403 application/json
{
  "error_code": "UNAUTHORIZED_ACTION",
  "error_message": "Requested action is unavailable or forbidden.",
  "details": null,
  "source": {
    "system": null,
    "code": null,
    "message": null
  }
}
```

```json 404 application/json
{
  "error_code": "RESOURCE_NOT_FOUND",
  "error_message": "Resource not found.",
  "details": null,
  "source": {
    "system": null,
    "code": null,
    "message": null
  }
}
```

```json 500 application/json
{
  "error_code": "SYSTEM_ERROR",
  "error_message": "Internal system error.",
  "details": null,
  "source": {
    "system": "TSYS",
    "code": "96",
    "message": "System malfunction"
  }
}
```

A reporting read is not identical to the create response, and the differences are deliberate:

* **`card_type` has a real value here.** On the create response it is always `UNKNOWN`, because the processor does not report it on the authorization.
* **`is_settled` and `batch_id` are current**, rather than frozen at the moment of the request.
* **`payment_method.entry_method` reads `keyed`** for a card number that was typed in. The create response echoes the `ECOMMERCE` or `MOTO` you sent.
* **`initiator` comes back null** on this endpoint. That is a known gateway defect, not a change of meaning — trust the create response for it.


## Search transactions

The filters:

| Parameter | Notes |
|  --- | --- |
| `limit` | Page size, default 100 |
| `offset` | Records to skip, default 0 |
| `created.gte`, `created.lte` | ISO 8601, UTC, inclusive |
| `result` | `APPROVED`, `DECLINED`, `ERROR` |
| `type` | Comma-separated list, case-insensitive. Read the caveats below |
| `card_type` | `CREDIT`, `DEBIT`, `UNKNOWN`. Comma-separated list |
| `integration` | `TSYS` or `VERICHECK` — uppercase here |
| `client_id` | For a reseller reading one merchant |
| `batch_id` | Everything in one batch |
| `is_settled` | Boolean |
| `sort_by` | `CREATED`, `TYPE`, `RESULT`. Default `CREATED` |
| `sort_order` | `ASC`, `DESC`. Default `DESC` |


## A filter that will mislead you

**A `200` is not proof a filter worked.** This one is worth knowing before you build a reconciliation process on it.

* **`card_type` is usually `UNKNOWN`.** Filtering on `CREDIT` or `DEBIT` alone quietly drops most rows. Include `UNKNOWN` unless you specifically want the determined ones: `card_type=CREDIT,DEBIT,UNKNOWN`.


The valid `type` values are the response vocabulary, not the create one: `SALE`, `AUTH`, `INCREMENTAL_AUTH`, `PAYOUT`, `REFUND`, `CANCEL`, `CAPTURE`. `VOID` and `PAYMENT` are not among them, and sending either returns `400 INVALID_REQUEST_DATA`. A voided payment is reported as `CANCEL`, so filter on that.

## Paging

The response wraps the results:

```json
{
  "total_count": 42,
  "limit": 100,
  "offset": 20,
  "has_more": true,
  "transactions": [ ... ]
}
```

Page by incrementing `offset` by `limit` while `has_more` is `true`.

Offset paging shifts when new transactions arrive mid-walk, and the default sort is newest first, so a long-running export can see a row twice or miss one. **Pin the window with `created.lte`** before you start paging, and the set stops moving under you.

## Finding ACH returns

A return arrives days after approval and nothing tells you. Poll for it.

Query the window you care about and check `is_returned` on each transaction. Remember that `return_code` and `return_reason` are **omitted rather than null** when there has been no return, so read them defensively.

## Next steps

* [Batch reports](/docs/after-the-payment/batch-reports)
* [Transaction lifecycle](/docs/after-the-payment/transaction-lifecycle)
* [Accept an ACH payment](/docs/payments/accept-an-ach-payment)