# Balance history

> The complete financial ledger of one currency balance: top-ups, booking debits and refunds.

Endpoint: `GET /api/v1/balances/{balance_id}/history`

Source: https://fx-port.com/docs/account/balance-history

`GET /api/v1/balances/{balance_id}/history`

**Permission:** Read

The money movements recorded against one balance: top-ups, booking debits and refunds, newest first. Call
[`GET /api/v1/balances`](https://fx-port.com/docs/account/balances.md) first to get the `balance_id` of the currency you want.

**Sandbox:** the answer is always empty (`pagination.total` is `0`) and `meta` says why.
**Live:** a `balance_id` that is not a balance of your agency, or that is not a UUID, is answered
`404 balance_not_found`.

## The `source` field

Each entry has a `source`:

| `source` | What it represents |
|---|---|
| `"transactions"` | **Authoritative balance movements.** Recorded every time the balance changes: top-up, booking charge or refund. **Use these for reconciliation totals.** eSIM orders and topups have no audit source: their debits are `transactions` rows. Transfers have no audit source either |
| `"flight_balance_operations"` | **Per-booking flight audit records.** Granular events of a booking (initial debit, exchange fee, adjustment, refund). One booking can produce several rows |
| `"hotel_balance_operations"` | **Per-booking hotel audit records.** The same idea as the flight operations |

A single action, for example confirming a flight, produces **both** a `transactions` row and a
`flight_balance_operations` row, and both appear in this answer. **Sum only the `transactions` rows** for
financial totals. The `description` of an eSIM debit is `eSIM Order <order code>` (a topup: `eSIM TopUp <order code>`),
where the order code is the `order_id` of the booking.

## Parameters

| Parameter | Type | Description |
|---|---|---|
| `balance_id` (path) | string | The `id` of a balance from `GET /api/v1/balances` |
| `date_from` | string | Inclusive start. Any ISO 8601 spelling is read: a date (`2026-07-01` or `20260701`, 00:00:00 UTC) or a datetime (`2026-07-01T00:00:00Z`) with 1 to 9 fraction digits and an offset written `Z`, `+01:00` or `+0100` (in a URL write the `+` as `%2B`). Without an offset a datetime is UTC |
| `date_to` | string | Inclusive end, in the same spellings. A bare date means the end of that day (23:59:59.999999 UTC). Not before `date_from` |
| `type` | string | Keep one type. **Exact and case sensitive**: `CREDIT`, `DEBIT` or `REFUND` match `transactions`; `credit`, `debit` or `refund` match the two `*_balance_operations` sources. Up to 32 characters |
| `page` | integer | The page, starting at 1 (at most 10,000). Default 1. An empty value means not sent |
| `limit` | integer | Entries per page, 1 to **100**. Default 20. A larger value is refused with `422 validation_error`; an empty value means not sent |

**Legacy names** (still work; the response carries a `deprecated_field` warning; `date_from` and `date_to` win when both are sent):

| Legacy | Use instead |
|---|---|
| `from`, `from_date` | `date_from` |
| `to`, `to_date` | `date_to` |

Never send `agency_id`: an API key that does is refused with `400 agency_id_not_allowed`. A value that is not an ISO 8601
date (`01/10/2026`, an epoch number, `yesterday`) is `400 invalid_date`, and `date_from` after `date_to` is `400 invalid_date_range`.

## Movement types

| Type | Direction | When it appears |
|---|---|---|
| `CREDIT` / `credit` | Funds **in** | Manual top-ups, deposits returned when a hold is released |
| `DEBIT` / `debit` | Funds **out** | Booking confirmations, exchange fees, manual debit adjustments |
| `REFUND` / `refund` | Funds **in** | Full or partial refund after a booking is cancelled or voided |

Upper case values appear in `transactions` rows and lower case values in `*_balance_operations` rows.

## Request example


**cURL**

~~~bash
curl --get \
  --url https://api.fx-port.com/api/v1/balances/db002f7a-c136-4f32-b19d-38a2542f2af9/history \
  --header 'Authorization: Bearer fxp_test_YOUR_KEY' \
  --data-urlencode 'date_from=2026-10-01' \
  --data-urlencode 'date_to=2026-10-31' \
  --data-urlencode 'limit=50'
~~~

**Node.js**

~~~js
const balanceId = "db002f7a-c136-4f32-b19d-38a2542f2af9";
const params = new URLSearchParams({ date_from: "2026-10-01", date_to: "2026-10-31", limit: "50" });
const response = await fetch(`https://api.fx-port.com/api/v1/balances/${balanceId}/history?${params}`, {
  headers: { Authorization: "Bearer fxp_test_YOUR_KEY" },
});
const { data: entries, pagination } = await response.json();
// reconciliation: only the authoritative movements
const net = entries
  .filter((entry) => entry.source === "transactions")
  .reduce((sum, entry) => sum + (entry.type === "DEBIT" ? -entry.amount : entry.amount), 0);
~~~

**Python**

~~~python
import requests

balance_id = "db002f7a-c136-4f32-b19d-38a2542f2af9"
response = requests.get(
    f"https://api.fx-port.com/api/v1/balances/{balance_id}/history",
    params={"date_from": "2026-10-01", "date_to": "2026-10-31", "limit": 50},
    headers={"Authorization": "Bearer fxp_test_YOUR_KEY"},
    timeout=30,
)
body = response.json()
~~~


## Response example

The amounts are examples. A live answer:

~~~json
{
  "environment": "live",
  "success": true,
  "data": [
    {
      "source": "transactions",
      "id": "3e8b1c02-d7a4-4f5e-9b2a-1c0d8e7f3a91",
      "type": "CREDIT",
      "amount": 2500.0,
      "currency": "DZD",
      "description": "Balance top-up",
      "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9",
      "balance_id": "db002f7a-c136-4f32-b19d-38a2542f2af9",
      "createdAt": "2026-10-11T09:15:00.000000+00:00",
      "created_at": "2026-10-11T09:15:00.000000+00:00"
    },
    {
      "source": "flight_balance_operations",
      "id": "658185df-234c-43ba-8175-ba9a5abe6007",
      "type": "debit",
      "amount": 87.5,
      "currency": "DZD",
      "description": "Exchange fee debit for flight booking Y7PAEU",
      "bookingRecordId": "711371c3-00aa-4be2-b790-9f0ca2ccfdb7",
      "booking_record_id": "711371c3-00aa-4be2-b790-9f0ca2ccfdb7",
      "createdAt": "2026-10-09T14:30:00.000000+00:00",
      "created_at": "2026-10-09T14:30:00.000000+00:00"
    },
    {
      "source": "transactions",
      "id": "31936df0-299d-429e-b0f5-056a6c0587cc",
      "type": "DEBIT",
      "amount": 4925.44,
      "currency": "DZD",
      "description": "Booking confirmation: flight Y7PAEU (ALG → ORN)",
      "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9",
      "balance_id": "db002f7a-c136-4f32-b19d-38a2542f2af9",
      "createdAt": "2026-10-01T15:42:34.272792+00:00",
      "created_at": "2026-10-01T15:42:34.272792+00:00"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 3, "total_pages": 1, "hasMore": false, "has_more": false },
  "meta": {
    "agencyId": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
    "agency_id": "0b1c2d3e-4f50-4a6b-8c7d-9e0f1a2b3c4d",
    "balanceId": "db002f7a-c136-4f32-b19d-38a2542f2af9",
    "balance_id": "db002f7a-c136-4f32-b19d-38a2542f2af9",
    "balanceCurrency": "DZD",
    "balance_currency": "DZD",
    "environment": "live"
  }
}
~~~

## Field reference

| Field | Present when | Description |
|---|---|---|
| `source` | Always | Where the entry comes from: see the table above |
| `id` | Always | The entry UUID |
| `type` | Always | `CREDIT`, `DEBIT` or `REFUND` (upper case in `transactions`, lower case in operations) |
| `amount` | Always | The amount, always positive: `type` gives the direction |
| `currency` | Always | ISO 4217 code: always the currency of the balance |
| `description` | Always | A label with the booking reference and the action. It can be `null` |
| `balanceId` / `balance_id` | `transactions` only | The balance the movement was recorded against |
| `bookingRecordId` / `booking_record_id` | operations only | The internal `id` of the booking: use it to match the entry with the [booking history](https://fx-port.com/docs/account/history.md) |
| `createdAt` / `created_at` | Always | ISO 8601 datetime, UTC |
| `pagination.hasMore` / `has_more` | Always | `true` when another page can be served |
| `meta.balanceId`, `meta.balanceCurrency` | Live | The balance of the history and its currency. Twins: `balance_id`, `balance_currency` |

### How far back the pages go

`pagination.total` is the real number of entries that match. The pages are built from the **newest entries of
each source** (up to 500 per source), so the oldest entries of a long history cannot be reached by paging:
follow `has_more` and narrow the list with `date_from` and `date_to` to read an older period.


> [!NOTE]
> **Reconciliation tip.** A single booking event produces both a `transactions` row and a
> `flight_balance_operations` row. To compute the net change of a balance, sum only the `transactions` rows. To
> trace one booking, match the `bookingRecordId` of the operation rows with the `id` of the booking in the
> [history](https://fx-port.com/docs/account/history.md).


## Errors

| HTTP | `code` | Cause |
|---|---|---|
| `400` | `agency_id_not_allowed` | An API key sent `agency_id` |
| `400` | `invalid_date`, `invalid_date_range` | A date is not valid, or `date_from` is after `date_to` |
| `401` | `authentication_required`, `invalid_api_key` | Missing or invalid API key |
| `404` | `balance_not_found` | The `balance_id` is not a balance of your agency (live), or is longer than 64 characters (any environment) |
| `422` | `validation_error` | `page` is below 1 or above 10,000, or `limit` is above 100 |
| `429` | `rate_limit_exceeded` | Over 6,000 requests per minute for this key |
| `503` | `service_overloaded` | The service is shedding load for a moment (its database pool is full). Nothing was read: retry after the seconds in `Retry-After` (2 to 5) |
