Advanced
Void and Post Transactions
Learn how to replace transactions with Twisp's voidTransaction and postTransaction mutations.
Many transactions have multi-step lifecycles where funds are first authorized at a pending layer and later settle. To the end user these transactions are logically a single event, but there may actually be multiple ledgering events that occur to the model the lifecycle of the transaction. ISO-8583 card authorizations are the canonical example, highly simplified:
Step 1: Authorize
Step 2: Capture/Settle
This tutorial walks through modeling that lifecycle in Twisp. You will:
- define layered tran codes for pending and settled states
- post an authorization, inspect correlated transactions, and understand the metadata that ties them together
- void the authorization while posting the settled capture in a single mutation batch
- automate transaction id generation via
VOID_AND_POSTworkflow helper - build an activity feed index that hides void noise for end users
Prerequisites
- Access to Twisp’s Financial GraphQL API (for example via GraphiQL).
- Journal and account identifiers for the ledger you want to write to. The examples below reuse the demo IDs from the example setup script.
- Familiarity with posting an initial transaction: voiding requires the original
transactionId.
Replace UUID defaults in the snippets with values from your environment when running them against a live ledger.
Step 1 — Create layered tran codes
Tran codes define the accounting logic that postTransaction reuses. We will create one code to place a hold on the PENDING layer and another to settle on the SETTLED layer. Both share a correlationId so they can be queried as a single lifecycle.
mutation CreateLifecycleTranCodes(
$pendingTranCodeId: UUID! = "e62e2a14-ba73-11f0-a918-069b540ea27c"
$settledTranCodeId: UUID! = "ec828824-ba73-11f0-a35f-069b540ea27c"
) {
pending: createTranCode(
input: {
tranCodeId: $pendingTranCodeId
code: "SAMPLE_TRANSFER_PENDING"
description: "Place funds on hold at the pending layer."
metadata: { category: "Card" }
params: [
{ name: "crAccount", type: UUID, description: "Account to credit." }
{ name: "drAccount", type: UUID, description: "Account to debit." }
{ name: "amount", type: DECIMAL, description: "Authorized amount." }
{ name: "currency", type: STRING, description: "ISO-4217 currency." }
{ name: "effective", type: DATE, description: "Authorization date." }
{
name: "journalId"
type: UUID
description: "Journal that records this flow."
default: "c2881874-007e-43e1-85ef-c263e8e361aa"
}
{
name: "correlationId"
type: STRING
description: "Identifier shared across the lifecycle."
}
{
name: "metadata"
type: JSON
description: "Optional JSON payload for webhooks."
default: "{}"
}
]
transaction: {
journalId: "params.journalId"
effective: "params.effective"
correlationId: "params.correlationId"
description: "'Authorization hold for ' + string(params.amount) + ' ' + params.currency"
metadata: "params.metadata"
}
entries: [
{
accountId: "params.drAccount"
units: "params.amount"
currency: "params.currency"
entryType: "'AUTH_PENDING_DR'"
direction: "DEBIT"
layer: "PENDING"
}
{
accountId: "params.crAccount"
units: "params.amount"
currency: "params.currency"
entryType: "'AUTH_PENDING_CR'"
direction: "CREDIT"
layer: "PENDING"
}
]
}
) {
tranCodeId
code
}
settled: createTranCode(
input: {
tranCodeId: $settledTranCodeId
code: "SAMPLE_TRANSFER_SETTLED"
description: "Post the settled capture and release the hold."
metadata: { category: "Card" }
params: [
{ name: "crAccount", type: UUID, description: "Account to credit." }
{ name: "drAccount", type: UUID, description: "Account to debit." }
{ name: "amount", type: DECIMAL, description: "Captured amount." }
{ name: "currency", type: STRING, description: "ISO-4217 currency." }
{
name: "effective"
type: DATE
description: "Settlement posting date."
}
{
name: "journalId"
type: UUID
description: "Journal that records this flow."
}
{
name: "correlationId"
type: STRING
description: "Identifier shared across the lifecycle."
}
{
name: "metadata"
type: JSON
description: "Optional JSON payload for webhooks."
default: "{}"
}
]
transaction: {
journalId: "params.journalId"
effective: "params.effective"
correlationId: "params.correlationId"
description: "'Capture settled for ' + string(params.amount) + ' ' + params.currency"
metadata: "params.metadata"
}
entries: [
{
accountId: "params.drAccount"
units: "params.amount"
currency: "params.currency"
entryType: "'AUTH_CAPTURE_DR'"
direction: "DEBIT"
layer: "SETTLED"
}
{
accountId: "params.crAccount"
units: "params.amount"
currency: "params.currency"
entryType: "'AUTH_CAPTURE_CR'"
direction: "CREDIT"
layer: "SETTLED"
}
]
}
) {
tranCodeId
code
}
}
Record the two code values—they tie directly to the mutations we use next.
Step 2 — Post the pending authorization
Use postTransaction with the pending tran code. Correlation links follow-on transactions back to this authorization. The metadata parameter must be JSON-encoded (pass a JSON string or variable).
mutation PostPendingAuthorization(
$transactionId: UUID! = "0c970fb5-29e1-4c4f-87d0-b20557a19a5a"
$correlationId: String! = "purchase-1001"
$creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16"
$debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9"
$journalId: UUID! = "c2881874-007e-43e1-85ef-c263e8e361aa"
) {
authorize: postTransaction(
input: {
transactionId: $transactionId
tranCode: "SAMPLE_TRANSFER_PENDING"
params: {
crAccount: $creditAccountId
drAccount: $debitAccountId
journalId: $journalId
amount: "25.00"
currency: "USD"
effective: "2025-01-07"
correlationId: $correlationId
metadata: {
merchantMcc: "5812"
merchantName:"Coffee Bar"
}
}
}
) {
transactionId
correlationId
effective
entries(first: 2) {
nodes {
entryId
accountId
direction
layer
amount {
units
currency
}
}
}
}
}
After posting, confirm the correlation in a read query:
query AuthorizationLifecycle(
$journalId: String! = "c2881874-007e-43e1-85ef-c263e8e361aa"
$correlationId: String! = "purchase-1001"
) {
transactions(
index: { name: CORRELATION_ID }
where: {
journalId: { eq: $journalId }
correlationId: { eq: $correlationId }
}
first: 10
) {
nodes {
transactionId
voidOf
voidedBy
entries(first: 2) {
nodes {
layer
amount {
units
currency
}
}
}
}
}
}
You should see one PENDING transaction at this stage.
Step 3 — Void the hold and post the capture
When settlement arrives, void the original transaction and immediately post the capture. Because both operations are mutations you can execute them in a single GraphQL document so the integration stays idempotent.
mutation CaptureAuthorization(
$authorizationId: UUID! = "0c970fb5-29e1-4c4f-87d0-b20557a19a5a"
$captureId: UUID! = "4d0d1fa5-4409-4f23-8b00-2eee8369bb98"
$correlationId: String! = "purchase-1001"
$creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16"
$debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9"
$journalId: UUID! = "c2881874-007e-43e1-85ef-c263e8e361aa"
) {
voidPending: voidTransaction(id: $authorizationId) {
transactionId
voidOf
correlationId
}
capture: postTransaction(
input: {
transactionId: $captureId
tranCode: "SAMPLE_TRANSFER_SETTLED"
params: {
crAccount: $creditAccountId
drAccount: $debitAccountId
journalId: $journalId
amount: "25.00"
currency: "USD"
effective: "2025-01-08"
correlationId: $correlationId
metadata: {
merchantMcc: "5812"
merchantName:"Coffee Bar"
captureBatchId: "batch-450"
}
}
}
) {
transactionId
correlationId
voidOf
entries(first: 2) {
nodes {
layer
direction
amount {
units
currency
}
}
}
}
}
Re-run AuthorizationLifecycle and you will now see:
- the original transaction with
voidedBypopulated - a void transaction with
voidOfpointing back to the authorization - the settled capture on the
SETTLEDlayer
Step 4 — Post to a single identifier via VOID_AND_POST workflow
Twisp’s transfer workflow wraps the pattern above. The workflow keeps state by executionId and uses the VOID_AND_POST task to post and void as needed. After creating the tran codes in Step 1, you can orchestrate the end-to-end lifecycle like this:
mutation WorkflowVoidAndPost(
$executionId: UUID! = "5368ff5e-48b5-4c69-a6c3-d4efcf3804eb"
$creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16"
$debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9"
$journalId: UUID! = "c2881874-007e-43e1-85ef-c263e8e361aa"
$correlationId: String = "purchase-1002"
) {
pending: workflow {
execute(
input: {
workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4"
task: "VOID_AND_POST"
executionId: $executionId
params: {
tranCode: "SAMPLE_TRANSFER_PENDING"
amount: "25.00"
currency: "USD"
effective: "2025-01-07"
metadata: {
merchantMcc: "5812"
merchantName:"Coffee Bar"
}
crAccount: $creditAccountId
drAccount: $debitAccountId
journalId: $journalId
correlationId: $correlationId
}
}
) {
output {
state
}
}
}
voidAndPost: workflow {
execute(
input: {
workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4"
task: "VOID_AND_POST"
executionId: $executionId
params: {
tranCode: "SAMPLE_TRANSFER_SETTLED"
amount: "25.00"
currency: "USD"
effective: "2025-01-08"
metadata: {
merchantMcc: "5812"
merchantName:"Coffee Bar"
captureBatchId: "batch-450"
}
crAccount: $creditAccountId
drAccount: $debitAccountId
journalId: $journalId
correlationId: $correlationId
}
}
) {
activities {
action
entityType
entity {
... on Transaction {
transactionId
correlationId
voidOf
entries(first: 2) {
nodes {
layer
amount {
units
currency
}
}
}
}
}
}
}
}
}
Note the workflowId and task parameters are fixed values.
Step 5 — Build a clean activity feed
End users expect to see a single line item even though the ledger contains three transactions (pending, void, and settled). Create a custom index that filters void/voided entries when you render an activity feed:
mutation CreateActivityFeedIndex {
schema {
createIndex(
input: {
name: "feed"
on: Entry
partition: [
{ alias: "journal_id", value: "document.journal_id" }
{ alias: "account_id", value: "document.account_id" }
]
sort: [
{ sort: ASC, alias: "created", value: "document.created" }
]
constraints: {
isNotVoidEntry: "!document.is_void_entry"
isNotVoidedEntry: "!document.is_voided_entry"
}
}
) {
id
}
}
}
Query the index with entries(index: { name: CUSTOM, custom: { name: "feed" } }, ...) to power a customer-facing feed that ignores internal bookkeeping noise.
You now have a full lifecycle: create layered tran codes, post an authorization, void and capture, utilize a workflow to ensure a single transaction effective for an id, and present a tidy history. Explore extending the workflow with additional states (for partial captures or declines) and add monitoring that alerts whenever a void happens outside of an expected settlement window.