Transaction Batching
Overview
A TransactionBatch groups
PaymentTransactions together for
settlement and payout. An owner has one open batch per batch bucket, which for most owners
means a single open batch. New transactions accumulate into it until it closes. Closing a batch
on demand opens a replacement immediately; a batch that closes on its own schedule gets no
replacement, and the next transaction to arrive opens one.
A batch's owner is either a Merchant or a
Payfac
(TransactionBatchOwner). A sub-merchant
is itself a Merchant — reachable from its payfac via Payfac.submerchants — so this union
already covers merchant, sub-merchant, and payfac batch owners.
Batch statuses
A batch starts OPEN and accumulates transactions. Closing it (see
Close a batch for payout) moves it to CLOSE_REQUESTED, and it
proceeds through settlement, clearing, and disbursement toward PAYOUT_COMPLETE. Batches can
also land in holding or terminal states.
The TransactionBatchStatus values:
| Status | Meaning |
|---|---|
OPEN | The batch is currently open and accepting transactions. |
CLOSE_REQUESTED | A close has been requested for the batch. |
PROCESSING | The batch is processing. |
ON_HOLD | The batch is on hold. |
CLOSED | The batch is closed. |
CLEAR_PENDING | The batch is pending clearing. |
CLEARED | The batch has cleared. |
DISBURSEMENT_PENDING | The batch is pending disbursement. |
PAYOUT_COMPLETE | The batch payout is complete. |
REJECTED | The batch was rejected. |
DISBURSED | The batch was disbursed. |
PAYOUT_PENDING | The batch payout is pending. |
PAYOUT_FAILED | The batch payout failed. |
How a batch accumulates
Highnote adds a transaction's amount and any associated fees to its batch's running totals as
the transaction moves through the payment lifecycle — for example, as a purchase is captured
and cleared, as funds are disbursed, or if a first chargeback is filed against a transaction in
the batch. A batch's debitTotal and
creditTotal total the transactions that debit or credit the customer. chargebackTotal is a
breakdown rather than a separate bucket — chargebacks are also counted in debitTotal or
creditTotal by direction, so summing all three double-counts them. currentTotal is the
signed running total of all transactions before fees, holds, and adjustments; it is null for
batches created before the field was introduced. disbursedAmount is what the owner is actually
paid, and it is currentTotal less fees, holds, and adjustments — so it will not reconcile against
currentTotal alone.
List transaction batches
Use the transactionBatches query to list an owner's batches — the open one, historical ones,
or a filtered slice by status or date. Results are scoped to your API key's organization
context. If you're a payfac, a query without further scoping returns your own batch plus one
for each of your sub-merchants — scope to a single owner with ownerId (see the
Highnote Query Language (HQL) fields below), or select owner
to tell them apart. If you build that filter from a value supplied by one of your sub-merchants,
validate its format before interpolating it — see
Build query strings safely.
transactionBatches(
first: Int = 20
after: String
filterBy: TransactionBatchFilterInput
): TransactionBatchConnectionPayload
TransactionBatchConnectionPayload is a union of AccessDeniedError, UserError, and
TransactionBatchConnection — select on the concrete type with ... on.
TransactionBatchFilterInput.searchQueryLanguage accepts an HQL query string and cannot be
combined with other filters.
The open batch
Most owners have a single open batch, so filtering on status = 'OPEN' returns it (plus one per
sub-merchant, for a payfac). An owner routed across several batch buckets has one open batch per
bucket, and an owner whose last batch closed on schedule has none until its next transaction
arrives:
List the open batch
Query
query QueryOpenTransactionBatch($filterBy: TransactionBatchFilterInput!) {
transactionBatches(filterBy: $filterBy) {
__typename
... on AccessDeniedError {
message
}
... on UserError {
errors {
errorPath
code
description
}
}
... on TransactionBatchConnection {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
owner {
__typename
... on Merchant {
id
}
... on Payfac {
id
}
}
status
debitTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
creditTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
chargebackTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
holdsTotalAmount {
value
currencyCode
decimalPlaces
}
adjustmentsTotalAmount {
value
currencyCode
decimalPlaces
}
}
}
}
}
}
Variables
{
"filterBy": {
"searchQueryLanguage": {
"query": "status = 'OPEN'",
"version": "VERSION_1"
}
}
}
Result
{
"data": {
"transactionBatches": {
"__typename": "TransactionBatchConnection",
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "<CURSOR_TRANSACTION_BATCH_OPEN>",
"endCursor": "<CURSOR_TRANSACTION_BATCH_PAYFAC_OPEN>"
},
"edges": [
{
"cursor": "<CURSOR_TRANSACTION_BATCH_OPEN>",
"node": {
"id": "<TRANSACTION_BATCH_ID_OPEN>",
"owner": {
"__typename": "Merchant",
"id": "<MERCHANT_ID>"
},
"status": "OPEN",
"debitTotal": {
"count": 2,
"totalAmount": {
"value": 10000,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"creditTotal": {
"count": 1,
"totalAmount": {
"value": 2500,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"chargebackTotal": {
"count": 0,
"totalAmount": {
"value": 0,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"holdsTotalAmount": {
"value": -100,
"currencyCode": "USD",
"decimalPlaces": 2
},
"adjustmentsTotalAmount": {
"value": -50,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
},
{
"cursor": "<CURSOR_TRANSACTION_BATCH_PAYFAC_OPEN>",
"node": {
"id": "<TRANSACTION_BATCH_ID_PAYFAC_OPEN>",
"owner": {
"__typename": "Payfac",
"id": "<PAYFAC_ID>"
},
"status": "OPEN",
"debitTotal": {
"count": 1,
"totalAmount": {
"value": 4000,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"creditTotal": {
"count": 0,
"totalAmount": {
"value": 0,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"chargebackTotal": {
"count": 0,
"totalAmount": {
"value": 0,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"holdsTotalAmount": {
"value": 0,
"currencyCode": "USD",
"decimalPlaces": 2
},
"adjustmentsTotalAmount": {
"value": 0,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
}
]
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}
TransactionBatchFilterInput supports five HQL fields:
| Field | Type | Operators | Example |
|---|---|---|---|
id | ID | =, != | id = 'acqtb_open_1' |
ownerId | ID | =, != | ownerId = 'acqmh_merchant_1' |
status | TransactionBatchStatus | =, != | status = 'OPEN' |
currentTotalAmount | Currency amount | =, !=, >=, <=, >, < | currentTotalAmount >= 'USD 300' |
createdAt | ISO 8601 timestamp | =, !=, >=, <=, >, < | createdAt >= '2025-12-09T00:00:00Z' |
An HQL money literal carries the amount in the currency's minor units, so
currentTotalAmount >= 'USD 300' matches batches at or above $3.00, not $300. The comparison
also scopes results to the currency you name.
So in addition to status = OPEN, you can filter to a specific status such as
status = 'ON_HOLD', exclude one with status != 'OPEN' (below), scope to a single owner with
ownerId, or filter by creation date or running total.
Non-open batches
Filter on status != OPEN to list an owner's historical and in-flight batches:
List non-open batches
Query
query QueryNonOpenTransactionBatch($filterBy: TransactionBatchFilterInput!) {
transactionBatches(filterBy: $filterBy) {
__typename
... on AccessDeniedError {
message
}
... on UserError {
errors {
errorPath
code
description
}
}
... on TransactionBatchConnection {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
status
}
}
}
}
}
Variables
{
"filterBy": {
"searchQueryLanguage": {
"query": "status != 'OPEN'",
"version": "VERSION_1"
}
}
}
Result
{
"data": {
"transactionBatches": {
"__typename": "TransactionBatchConnection",
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "<CURSOR_TRANSACTION_BATCH_PROCESSING>",
"endCursor": "<CURSOR_TRANSACTION_BATCH_ON_HOLD>"
},
"edges": [
{
"cursor": "<CURSOR_TRANSACTION_BATCH_PROCESSING>",
"node": {
"id": "<TRANSACTION_BATCH_ID_PROCESSING>",
"status": "PROCESSING"
}
},
{
"cursor": "<CURSOR_TRANSACTION_BATCH_CLOSED>",
"node": {
"id": "<TRANSACTION_BATCH_ID_CLOSED>",
"status": "CLOSED"
}
},
{
"cursor": "<CURSOR_TRANSACTION_BATCH_ON_HOLD>",
"node": {
"id": "<TRANSACTION_BATCH_ID_ON_HOLD>",
"status": "ON_HOLD"
}
}
]
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}
Inspect a batch
A TransactionBatch implements Node, so once you have
a batch's id — from a list result above, or from another reference — fetch it directly
through the top-level node query. The example below inspects a closed batch, because fees and
disbursedAmount are produced by the close workflow and are null while a batch is still open:
Inspect a batch
Query
query QueryTransactionBatchNode($id: ID!) {
node(id: $id) {
__typename
... on TransactionBatch {
id
owner {
__typename
... on Merchant {
id
}
... on Payfac {
id
}
}
status
createdAt
closeRequestedAt
currentTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
debitTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
creditTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
chargebackTotal {
count
totalAmount {
value
currencyCode
decimalPlaces
}
}
holdsTotalAmount {
value
currencyCode
decimalPlaces
}
adjustmentsTotalAmount {
value
currencyCode
decimalPlaces
}
disbursedAmount {
value
currencyCode
decimalPlaces
}
fees {
__typename
accountingDirection
description
feeAmount {
value
currencyCode
decimalPlaces
}
}
}
}
}
Variables
{
"id": "<TRANSACTION_BATCH_ID_CLOSED>"
}
Result
{
"data": {
"node": {
"__typename": "TransactionBatch",
"id": "<TRANSACTION_BATCH_ID_CLOSED>",
"owner": {
"__typename": "Merchant",
"id": "<MERCHANT_ID>"
},
"status": "CLOSED",
"createdAt": "2026-07-01T00:00:00.000Z",
"closeRequestedAt": "2026-07-08T00:00:00.000Z",
"currentTotal": {
"count": 3,
"totalAmount": {
"value": 7500,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"debitTotal": {
"count": 2,
"totalAmount": {
"value": 10000,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"creditTotal": {
"count": 1,
"totalAmount": {
"value": 2500,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"chargebackTotal": {
"count": 0,
"totalAmount": {
"value": 0,
"currencyCode": "USD",
"decimalPlaces": 2
}
},
"holdsTotalAmount": {
"value": -100,
"currencyCode": "USD",
"decimalPlaces": 2
},
"adjustmentsTotalAmount": {
"value": -50,
"currencyCode": "USD",
"decimalPlaces": 2
},
"disbursedAmount": {
"value": 7200,
"currencyCode": "USD",
"decimalPlaces": 2
},
"fees": [
{
"__typename": "TransactionBatchProcessingFee",
"accountingDirection": "DEBIT",
"description": "Processing fee",
"feeAmount": {
"value": 150,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
]
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}
Drill into a batch's transactions
The transactions field on TransactionBatch returns the PaymentTransactions inside a
batch, letting you break it down by type or by amount. Results are always scoped to the batch
you're querying — the batchId filter is applied automatically, so you never supply it
yourself. Alias the field (purchases: / refunds: below) to pull multiple filtered slices in
one request.
Drill into a batch's transactions
Query
query QueryTransactionBatchByTransactionType($id: ID!) {
node(id: $id) {
__typename
... on TransactionBatch {
id
status
purchases: transactions(
filterBy: {
searchQueryLanguage: {
query: "accountingDirection = 'DEBIT'"
version: VERSION_1
}
}
) {
__typename
... on AccessDeniedError {
message
}
... on UserError {
errors {
errorPath
code
description
}
}
... on PaymentTransactionsConnection {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
__typename
... on PaymentDebitTransaction {
id
capturedAmount {
value
currencyCode
decimalPlaces
}
}
}
}
}
}
refunds: transactions(
filterBy: {
searchQueryLanguage: {
query: "accountingDirection = 'CREDIT'"
version: VERSION_1
}
}
) {
__typename
... on AccessDeniedError {
message
}
... on UserError {
errors {
errorPath
code
description
}
}
... on PaymentTransactionsConnection {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
__typename
... on PaymentCreditTransaction {
id
capturedAmount {
value
currencyCode
decimalPlaces
}
}
}
}
}
}
}
}
}
Variables
{
"id": "<TRANSACTION_BATCH_ID_OPEN>"
}
Result
{
"data": {
"node": {
"__typename": "TransactionBatch",
"id": "<TRANSACTION_BATCH_ID_OPEN>",
"status": "OPEN",
"purchases": {
"__typename": "PaymentTransactionsConnection",
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "<CURSOR_PAYMENT_DEBIT_1>",
"endCursor": "<CURSOR_PAYMENT_DEBIT_2>"
},
"edges": [
{
"cursor": "<CURSOR_PAYMENT_DEBIT_1>",
"node": {
"__typename": "PaymentDebitTransaction",
"id": "<PAYMENT_DEBIT_TRANSACTION_ID_1>",
"capturedAmount": {
"value": 6000,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
},
{
"cursor": "<CURSOR_PAYMENT_DEBIT_2>",
"node": {
"__typename": "PaymentDebitTransaction",
"id": "<PAYMENT_DEBIT_TRANSACTION_ID_2>",
"capturedAmount": {
"value": 4000,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
}
]
},
"refunds": {
"__typename": "PaymentTransactionsConnection",
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "<CURSOR_PAYMENT_CREDIT_1>",
"endCursor": "<CURSOR_PAYMENT_CREDIT_1>"
},
"edges": [
{
"cursor": "<CURSOR_PAYMENT_CREDIT_1>",
"node": {
"__typename": "PaymentCreditTransaction",
"id": "<PAYMENT_CREDIT_TRANSACTION_ID_1>",
"capturedAmount": {
"value": 2500,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
}
]
}
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}
TransactionBatchEntryFilterInput supports five HQL fields:
| Field | Type | Operators | Example |
|---|---|---|---|
publicPaymentId | ID | =, != | publicPaymentId = 'acqt_sale_1' |
capturedAmount | Currency amount | =, !=, >=, <=, >, < | capturedAmount >= 'USD 300' |
accountingDirection | DEBIT, CREDIT | =, != | accountingDirection = 'DEBIT' |
createdAt | ISO 8601 timestamp | =, !=, >=, <=, >, < | createdAt >= '2025-12-09T00:00:00Z' |
updatedAt | ISO 8601 timestamp | =, !=, >=, <=, >, < | updatedAt >= '2025-12-09T00:00:00Z' |
Money literals here carry minor units too, so capturedAmount >= 'USD 300' AND capturedAmount <= 'USD 500' matches entries between $3.00 and $5.00.
Batch-level fees
A batch's fees field returns
TransactionBatchFee entries — a
separate interface from the transaction-level
PaymentTransactionFee described in
Transaction Fees. TransactionBatchFee declares
accountingDirection, description, and feeAmount — one field more than
PaymentTransactionFee, which exposes only accountingDirection and feeAmount. It currently has
one concrete type,
TransactionBatchProcessingFee.
Batch fees are calculated when a batch closes, so they are populated on closed batches and null
on an open one — note that the field is null rather than an empty list when a batch has no fees.
See the fees selection in Inspect a batch above for a populated example.
Close a batch for payout
Close an open batch on demand with closeTransactionBatch. The close is processed
asynchronously: the batch moves to CLOSE_REQUESTED immediately and doesn't reach CLOSED
until the async processing completes. Closing also opens a new batch in its place, returned as
openedTransactionBatch.
Whether a batch also closes on a schedule depends on its owner. A batch owned by a Payfac closes
automatically each day at the globally configured close time, but closing one on demand opts that
owner out of the schedule — the replacement batch returned as openedTransactionBatch then stays
open until it too is closed on demand. Batches owned by a Merchant, including a payfac's
sub-merchants, close automatically only when auto-close is configured for the account; otherwise
they stay open until closed on demand.
The input takes transactionBatchId (required), plus optional idempotencyKey (a UUIDv4 that
dedupes repeated requests) and externalIdentifier (an external reference, up to 255
alphanumeric characters).
This mutation runs against your active organization's environment. Closing a batch cannot be
undone — there is no reopen operation — and closing a Payfac-owned batch on demand permanently
opts that owner out of the daily close schedule.
Close a batch for payout
Query
mutation CloseTransactionBatch($input: CloseTransactionBatchInput!) {
closeTransactionBatch(input: $input) {
__typename
... on AccessDeniedError {
message
}
... on UserError {
errors {
errorPath
code
description
}
}
... on CloseTransactionBatch {
closingTransactionBatch {
id
status
fees {
__typename
accountingDirection
description
feeAmount {
value
currencyCode
decimalPlaces
}
}
}
openedTransactionBatch {
id
status
}
}
}
}
Variables
{ "input": { "transactionBatchId": "<TRANSACTION_BATCH_ID_OPEN>", "idempotencyKey": "<IDEMPOTENCY_KEY>", "externalIdentifier": "<EXTERNAL_IDENTIFIER>" } }
Result
{
"data": {
"closeTransactionBatch": {
"__typename": "CloseTransactionBatch",
"closingTransactionBatch": {
"id": "<TRANSACTION_BATCH_ID_OPEN>",
"status": "CLOSE_REQUESTED",
"fees": [
{
"__typename": "TransactionBatchProcessingFee",
"accountingDirection": "DEBIT",
"description": "Processing fee",
"feeAmount": {
"value": 150,
"currencyCode": "USD",
"decimalPlaces": 2
}
}
]
},
"openedTransactionBatch": {
"id": "<TRANSACTION_BATCH_ID_NEW>",
"status": "OPEN"
}
}
},
"extensions": {
"requestId": "<REQUEST_ID>"
}
}