# Balances With compute-on-write rollup calculations, balances always reflect the most up-to-date state of an account. Source: https://www.twisp.com/docs/accounting-core/balances ## Accounts and Balances Balances are auto-calculated sums of the entries for a given account. Every balance record maintains a `drBalance` for entries on the DEBIT side of the ledger and a `crBalance` for entries on the CREDIT side of the ledger. In addition, a `normalBalance` is calculated as the difference of `credits - debits` for credit normal accounts or as `debits - credits` for debit normal accounts. See [Chart of Accounts: Credit Normal and Debit Normal](/docs/accounting-core/chart-of-accounts#credit-normal-and-debit-normal). ### A Balance for Every Journal, Currency, and Layer Accounts have separate balances for every journal, currency, and for each of the three layers: `SETTLED`, `PENDING`, and `ENCUMBRANCE` (see [Layered Accounting](/docs/accounting-core/layered-accounting)). In a simple one-journal, one-currency ledger that only uses the `SETTLED` and `PENDING` layers, accounts will materialize two balances (one for each layer). More complex ledgers which have multiple journals and currencies will compute more balances for each account. For example, accounts in a ledger with 2 journals, 2 currencies, and using all 3 layers would have 12 balances (2 * 2 * 3). ## Balance Calculations Balances in Twisp are derived from entries in response to changes in the ledger. These balance calculations are computed on write, not on read. This means that we can query balances like any other piece of data in the system and have 100% certainty that the balance reflects the current state of the ledger. ### Calculating Normal Balance To illustrate how normal balances are calculated, let's look at some example tables. Say we have a ledger with two accounts: 1. **Cash** (debit normal) 2. **Revenue** (credit normal) Next, let's assume the following entries have posted to our ledger: | Entry ID | Account | Amount | Direction | |----------|---------|--------|-----------| | 1 | Revenue | $500 | CREDIT | | 2 | Cash | $500 | DEBIT | | 3 | Revenue | $400 | DEBIT | | 4 | Cash | $400 | CREDIT | | 5 | Revenue | $250 | CREDIT | | 6 | Cash | $250 | DEBIT | Given this set of entries, can calculate the normal balances for the Cash and Revenue accounts by taking the sum of their credits and debits, and subtracting one from the other according to their normal balance type. Here's what the balances table would look like: **Balances** | Account | CR Balance | DR Balance | Normal Balance | |---------|------------|------------|----------------| | Cash | $400 | $750 | $350 | | Revenue | $750 | $400 | $350 | Note how the sum of all credits equals the sum of all debits, so we know that this balance sheet is consistent with double-entry accounting principles. The normal balance gives us useful context-dependent information about the account. For the Cash account, it tells us how much cash we currently have on hand. For the Revenue account, it tells us the net revenues we've earned so far. ## Debits and Credits (DR/CR) Each entry in a journal either debits or credits an account. Along with a DEBIT/CREDIT direction, the amount of the entry is a signed number. Both negative and positive debits or credits are possible. The primary reason for this setup is to _make the debit and credit balances for an account meaningful_. Consider an example where we would like our credit balance to accurately reflect all deposits against an account: | Entry ID | Account ID | Type | Debit | Credit | |----------|------------|---------|-------|----------| | 1 | f29f83 | DEPOSIT | - | $1000.00 | DEBIT BALANCE: **$0.00**\ CREDIT BALANCE: **$1000.00**\ NORMAL BALANCE: **$1000.00** In this case our credit balance is $1000. In other words, we've deposited $1000 to this account. We'll assume that the account in question is a credit normal account, meaning that the normal balance is calculated by subtracting debits from credits. Now, let's say a mistake was made and that transaction amount should have actually been $1200 and we need to correct this. Because the ledger is immutable and append-only, we cannot erase or change the amount of the original transaction. To rectify this situation in a way that accurately records the history of events, we _void the transaction_ and repost the corrected version. | Entry ID | Account ID | Type | Debit | Credit | |----------|------------|--------------|-------|------------| | 1 | f29f83 | DEPOSIT | - | $1000.00 | | 2 | f29f83 | VOID_DEPOSIT | - | $(1000.00) | | 3 | f29f83 | DEPOSIT | - | $1200.00 | DEBIT BALANCE: **$0.00**\ CREDIT BALANCE: **$1200.00**\ NORMAL BALANCE: **$1200.00** In this scenario above, the credit balance is now $1200. Exactly what we'd expect. Let's consider an alternate solution: what if we had simply utilized debits and credits to achieve the same resulting account balance? | Entry ID | Account ID | Type | Debit | Credit | |----------|------------|--------------|----------|----------| | 1 | f29f83 | DEPOSIT | - | $1000.00 | | 2 | f29f83 | VOID_DEPOSIT | $1000.00 | - | | 3 | f29f83 | DEPOSIT | - | $1200.00 | DEBIT BALANCE: **$1000.00**\ CREDIT BALANCE: **$2200.00**\ NORMAL BALANCE: **$1200.00** In this incorrect version, the _credit balance_ is now $2200 along with a _debit balance_ of $1000. This effectively lost meaning of the sum total debit and credit balances, because money wasn't _actually_ moving out of the account. We were just cancelling > **Note:** > > For the sake of simplicity in illustration, we did not specify the layer used. We can assume that they all occurred on the "SETTLED" layer. > > Learn more about layers in [Layered Accounting](/docs/accounting-core/layered-accounting). --- # Chart of Accounts The chart of accounts is the basis for creating balance sheets, P&L reports, and for understanding the balances for the customer and business entities your business services. Source: https://www.twisp.com/docs/accounting-core/chart-of-accounts ## Accounts Overview [Accounts](/docs/reference/graphql/types/object#account) are a named **store of value** in that every account has a [Balance](/docs/reference/graphql/types/object#balance), as well as a **record of activity** in the form of ledger entries posted to them. In Twisp, accounts record activity and materialize balances across multiple **layers** (see [Layered Accounting](/docs/accounting-core/layered-accounting)) and have full support for multiple [Journals](/docs/reference/graphql/types/object#journal) Like every other record in the system, accounts are stored as immutable documents with all changes to them stored in a versioned history. Say we had a wallet product. We can model each customer's wallet as an account, and the full collection of those accounts plus any other accounts we need to track money makes up our **chart of accounts**. | Name | Dr Balance | Cr Balance | Normal Balance | | --- | --- | --- | --- | | Ali | $2.30 | $9.20 | $6.90 | | Bea | $5.00 | $6.00 | $1.00 | | Cal | $1.80 | $8.50 | $6.70 | | ... | ... | ... | ... | ## Organizing Accounts with Sets Many accounting systems model the chart of accounts as a single hierarchical tree, with "parent" accounts and "child" or sub-accounts. This is especially useful to do things like roll up balances across multiple accounts. However, a single structure for _every_ account can be limiting, as some use cases call for multiple independent ways of organizing accounts. In Twisp, we use a more flexible concept of [AccountSets](/docs/reference/graphql/types/object#account-set), which are custom groups of accounts that aggregate balances and provide a unified interface into the entries for all accounts. | Account Set | Accounts | Debit Balance | Credit Balance | Normal Balance | |-------------|----------|---------------|----------------|----------------| | A | Ali, Cal | $4.10 | $17.70 | $13.60 | | B | Bea | $5.00 | $6.00 | $1.00 | | ... | ... | ... | ... | ... | One important feature of sets is that they can **contain other sets**. By nesting sets in this way, can model more complex structures for a chart of accounts. Let's take the above accounts and imagine that they represent FBO accounts that we manage on behalf of our customers. With account sets, we could create a chart of accounts with these relationships: ```mermaid graph BT ASS[/Assets\] CAS[/Cash\] ACH[ACH Receivable] INV[Investments] LIA[/Liabilities\] FBO[/FBO Accounts\] Ali Bea Cal ACH --> CAS --> ASS INV --> ASS Ali & Bea & Cal --> FBO --> LIA ``` In this chart, there are only four accounts: "ACH Receivable", "Investments", and the FBO accounts for Ali, Bea, and Cal. Organizing these accounts into various sets lets us materialize balances for different combinations of accounts. > **Note:** > > Entries can only be posted to _accounts_, not to account _sets_. ## Balances Roll Up Entries All accounts have corresponding [Balances](/docs/reference/graphql/types/object#balance) to summarize the amounts of entries posted to the account. For example, Bea's account could have the following entries: | Amount | Direction | Posted On | |--------|-----------|------------| | $4.00 | CREDIT | 2022-09-15 | | $2.00 | CREDIT | 2022-09-19 | | $5.00 | DEBIT | 2022-09-20 | Adding these up, we can see how her account reflects a **debit balance** of **$5.00** and a **credit balance** of **$6.00**, resulting in a **normal balance** of **$1.00**. We can use account sets to roll up balances for different groupings of accounts. For example, if the "FBO Accounts" set corresponds the balance of a single account at a banking partner, it can be used to do reconciliation confirming that the balances of your FBO accounts match up to the balance shown by the bank. > **Note:** > > The balance of account set X is alway equal to the sum of all entries posted to accounts in X plus the balances of all account sets in X. To illustrate this concept, let's see how each of the account sets in the chart above would calculate their balances. To begin, we need to know the balances for all accounts. We'll focus here on just normal balances to keep things simple. | Account | Normal Balance | |----------------|----------------| | ACH Receivable | $14.60 | | Investments | $72.67 | | Ali | $6.90 | | Bea | $1.00 | | Cal | $6.70 | Next, we can calculate each account set's balance by adding up the balances of its member accounts and account sets. | Account Set | Members | Member Balances | Account Set Balance | |--------------|-------------------|---------------------|---------------------| | Cash | ACH Receivable | $14.60 | $14.60 | | FBO Accounts | Ali, Bea, Cal | $6.90, $1.00, $6.70 | $14.60 | | Assets | Cash, Investments | $14.60, $72.67 | $82.27 | | Liabilities | FBO Accounts | $14.60 | $14.60 | ## Credit Normal and Debit Normal In double-entry accounting, accounts are often differentiated between "credit normal" and "debit normal". This means is that the balance for an account is either computed as `credits - debits` for credit normal accounts, and as `debits - credits` for debit normal accounts. Debit normal accounts are often used for asset and expense account types where debits indicate money flowing _in_ to the account and thus _increasing_ its balance, and credits represent money flowing _out_ and _decreasing_ the balance. Credit normal accounts are often used for accounts which track things like revenue, liability, and equity where credits _increase_ the balance and debits _decrease_ the balance. > **Note:** > > Read more about how account balances are calculated on the [Balances](/docs/accounting-core/balances) page. Twisp imposes no strict rules about which accounts can or should be credit or debit normal, as the design for a product ledger's chart of accounts will vary depending on the specifics needs of the product. Some users may decide to impose the standard 5 account types: assets, liabilities, expenses, revenue, and equity. Others may need a modified structure, or may only need credit normal accounts. The accounting core can structured to accommodate any variety. ## Creating Accounts You can provision Twisp with a chart of accounts for a number of business use cases. Twisp can generate a chart of accounts suitable for representing economic activities for use cases such as: - Digital Banking and Card Issuing - Acquiring, Marketplace, Vertical SaaS - Lending - Currency Exchange > **Note:** > > You have full control over the chart of accounts and can create new settlement and other accounts to represent your own funds flows. An example of commonly-used accounts include a number of settlement accounts: | Account | Purpose | |----------------------|----------------------------------------------------------------------------------------------------| | Suspense Account | The suspense account is posted to when transactions cannot be booked against a valid open account. | | ACH Settlement | The ACH settlement account for settling ACH transactions. | | Bill Pay Settlement | An account for settling bill payments. | | Checks | Settlement account for checks that are posted to the system. | | ACH Reconciliation | An account for ACH reconciliations losses | | Charge off | Accounts for charging off accounts that we’re closing due to losses. | | Fraud Loss | Fraud losses are booked here. | | Courtesy Credit | Account used by customer service/experience for courtesy funds. | | Levies, garnishments | Account used for dealing with account levies and wage garnishments. | | Card Disputes | Account for issuing funds for card disputes. | | ACH Disputes | Account for ACH disputes. | --- # Encoded Transactions Design accounting logic with tran codes to create composable transaction types. Source: https://www.twisp.com/docs/accounting-core/encoded-transactions ## Transactions and Entries _Transactions_ record all accounting events in the ledger. **In Twisp, the only way to write to a ledger is through a transaction.** Every transaction writes two or more entries to the ledger in standard double-entry accounting practice. Twisp expands upon the basic principle of an accounting transaction with additional features like transaction codes and correlations. An _entry_ represents one side of a transaction in a ledger. In other systems, these may be called "ledger lines" or "journal entries". Entries always have an account, amount, and direction (CREDIT or DEBIT). In addition, Twisp uses the concept of "entry types" to assign every entry to a categorical type. Twisp enforces double-entry accounting, which in practice means that entries can only be entered in the context of a Transaction. Posting a transaction will create _at least 2_ ledger entries. In addition, we run validity checks against transactions to ensure that they do not introduce inconsistencies into the accounting core. For example, we ensure that the debit and credit entries written by a transaction sum to zero so that value is never lost or created from nothing. **By establishing a strict definition of how entries are written to the ledger, we ensure a high level of integrity and consistency in the ledger record.** ## Composable Double-Entry Accounting Every transaction entered must use a transaction code to indicate what _kind_ of transaction it is, which in turn determines how entries are written to the ledger. This applies both a strong categorization scheme to transactions as well as a valuable internal reference of transaction types for product engineers to draw upon. We think that transaction codes (tran codes) are the optimal way for engineers working on financial products do double-entry accounting. They encode the basic patterns for a type of transaction as a predictable and repeatable formula. You can think of tran codes as functions which define how a transaction acts upon the ledger. To better understand how tran codes work, let's look at an example. ## Tran Codes in Practice While the full API for tran codes allows for a large degree of flexibility, we'll focus on a simple case for the sake of illustration. Let's say we're building a product that needs to support tracking ACH credits, and reflecting when money is deposited to a customer's bank account. This transaction can be encoded with a tran code. When a new transaction is posted using this tran code, we want to make sure that: 1. A credit entry is written to the "ACH Settlement" account 2. A debit entry is written to the customer's account 3. The amount used for both entries is equal We'll use the entry type `ACH_CR` for the credit entry, and `ACH_DR` for the debit entry to be extra-clear about what these entries represent. Here's how we would create the tran code using GraphQL: **Request** ```graphql mutation BasicTranCode { createTranCode( input: { # Unique ID for the tran code tranCodeId: "53c7a411-9070-42c8-81cf-ea37ebe63182" # Unique name for the tran code code: "ACH_CREDIT" # Short description of what it does description: "An ACH credit into an account." # Params define the inputs to a transaction params: [ { name: "account", type: UUID, description: "Deposit account ID." } { name: "amount", type: DECIMAL } { name: "effectiveDate", type: DATE } ] # Values supplied to the Transaction transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effectiveDate" } # The ledger Entries to create entries: [ { # ID for the ACH settlement account accountId: "uuid('8cd11607-1104-4270-9482-ae4b8053fd5a')" # The `params` object allows runtime access to input values units: "params.amount" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId code entries { accountId units entryType direction layer } } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "53c7a411-9070-42c8-81cf-ea37ebe63182", "code": "ACH_CREDIT", "entries": [ { "accountId": "uuid('8cd11607-1104-4270-9482-ae4b8053fd5a')", "units": "params.amount", "entryType": "'ACH_DR'", "direction": "DEBIT", "layer": "SETTLED" }, { "accountId": "params.account", "units": "params.amount", "entryType": "'ACH_CR'", "direction": "CREDIT", "layer": "SETTLED" } ] } } } ``` Did you notice that `params.amount` value for the `amount` of each entry? This is a CEL expression which means "use the `amount` field on the `params` argument provided when a new transaction is posted. Because tran codes are essentially **templates for transactions**, this lets us dynamically set the amount field when we actually go to post a transaction with this tran code. With this tran code defined, we can now post a transaction to perform a deposit of $12.87: **Request** ```graphql mutation BasicTransaction { postTransaction( input: { transactionId: "a71dd074-3b4e-465b-80f4-9dc111a8ecb4" tranCode: "ACH_CREDIT" params: { account: "63e766a5-4a04-4aee-a4d6-aa49350f13c6" amount: "12.87" effectiveDate: "2022-09-21" } } ) { transactionId tranCode { code } entries(first: 4) { nodes { accountId units currency direction layer } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "a71dd074-3b4e-465b-80f4-9dc111a8ecb4", "tranCode": { "code": "ACH_CREDIT" }, "entries": { "nodes": [ { "accountId": "8cd11607-1104-4270-9482-ae4b8053fd5a", "units": "12.87", "currency": "USD", "direction": "DEBIT", "layer": "SETTLED" }, { "accountId": "63e766a5-4a04-4aee-a4d6-aa49350f13c6", "units": "12.87", "currency": "USD", "direction": "CREDIT", "layer": "SETTLED" } ] } } } } ``` By providing just a `tranCode` and `params` to the `postTransaction` call, we are able to make use of the predefined tran code to write two complete entries to the ledger. This is just a teaser of what you can do with tran codes. With their flexibility, you can encode nearly any kind of transaction your product needs to support. ### Example Tran Codes The set of tran codes you need will be specific to your company and product. There are often many "archetypal" tran codes which are commonly used. Some examples include: | TranCode | Description | Types of Entries Written | |------------------|----------------------------|------------------------------------------| | WIRE_TRANSFER | Bank-to-bank wire transfer | WIRE_OUTGOING_DR, WIRE_INCOMING_CR | | CARD_HOLD_CANCEL | Card hold cancellation | CARD_HOLD_CANCEL_DR, CARD_HOLD_CANCEL_CR | | DEPOSIT | Bank deposit | DEPOSIT_DR, DEPOSIT_CR | | BILL_PAYMENT | Bill payment | BILL_PAYMENT_DR, BILL_PAYMENT_CR | ## Timing and Sequencing Accurate recording of the times and sequences of events in a ledger is critical for auditing and reconciliation. ### Effective Dates There are two significant time-based values on a ledger transaction: the `created` timestamp and an `effective` date. | Field | Description | |-----------|---------------------------------------------------------| | Created | The wall time the transaction was posted to the ledger. | | Effective | The accounting date to which this transaction applies. | These two values often are not the same. For example, an ACH transaction may post over the weekend, but the `effective` accounting date of the transaction may be the following Monday. ### Entry Sequences Ledger entries are always posted in the order in which they are defined within a tran code. When a transaction is posted, it writes this ordering as a `sequence` onto every entry written. Within the context of a transaction, we can thus see a clear incremented sequence of all entries. Because transactions are written atomically at the database layer, every entry is posted at the same clock time. ## Embedding Meaning & Context When attempting to trace money movement, having as much contextual information as possible is useful to get a complete picture of what happened and when. In addition to the meaning and context implied by tran codes and entry types, additional information can be embedded into transactions through correlations and metadata. ### Correlation Identifiers With transactional workflows it is often necessary to group a set of related transactions. For example: during card processing there is often a hold, then a hold release or expiration, and finally a settlement. In Twisp, correlation identifiers are used to group these transactions together. When a transaction is posted without a `correlationId`, it uses its own `transactionId` as the `correlationId`. Then, future related transactions can be posted with the same `correlationId` to indicate their relationship to the original. This is very useful for events like holds, auths, auth reversals, etc. The transactions from the card processing example above might look like this: | ID | Amount | Description | Correlation ID | |----|--------|-------------------------|----------------| | 1 | $50 | Place card hold | 1 | | 2 | $50 | Release card hold | 1 | | 3 | $50 | Settle card transaction | 1 | Because transactions (2) and (3) are _related_ to transaction (1), they share the same correlation ID. This way, we can easily observe the entire history of a multi-transaction event by querying the correlated transactions. ### Transaction Metadata Transactions contain a `metadata` field which can store arbitrary structured data about the transaction in JSON format. This can be a highly useful way to embed application- and product-specific data in the transaction itself. It has no effect on the accounting operations. --- # The Accounting Core An accounting engine for building products that work with money. Source: https://www.twisp.com/docs/accounting-core We built the Accounting Core to serve as an engine for all kinds of financial products. It is designed to be flexible enough to cover any use case that you can imagine, and comes pre-packaged with a set of sensible defaults to give you a head start. When you provision a new instance of the Twisp Accounting Core, you get a **transactions ledger** for double-entry accounting, a **chart of accounts** to represent any economic activity, and **layered balances** for tracking settled, pending, and planned funds flows. All of this is accessible via a straighforward [GraphQL API](/docs/reference/graphql). Armed with these primitives, designers and developers will have a clear mental model for how to interact with, iterate on, and scale their products with foundational ledger tooling that gives confidence when working with mission-critical financial data. Continue reading to learn more about how each part of accounting core works: - [**Ledgers**](/docs/accounting-core/ledgers-in-twisp)\ The accounting core is built upon a single source-of-truth ledger. - [**Encoded Transactions**](/docs/accounting-core/encoded-transactions)\ Design accounting logic with tran codes to create composable transaction types. - [**Chart of Accounts**](/docs/accounting-core/chart-of-accounts)\ A chart of accounts models all of the economic activity that your ledger records. - [**Layered Accounting**](/docs/accounting-core/layered-accounting)\ Layers help clarify the true state of funds in accounts. - [**Balances**](/docs/accounting-core/balances)\ With compute-on-write rollup calculations, balances always reflect the most up-to-date state of an account. --- # Layered Accounting Layers help clarify the true state of funds in accounts. Source: https://www.twisp.com/docs/accounting-core/layered-accounting ## The Three Layer Model Twisp utilizes a layered model of accounting to distinguish transactions between three important categories: 1. The **settled** layer is for transactions that have fully settled. 2. The **pending** layer includes holds and pending transactions which have been authorized but not yet settled. 3. The **encumbrance** layer contains expected, planned, and scheduled future transactions. By segmenting transactions in this way, it provides the data integrity needed for more accurate and useful balance calculations and funds flow modelling. It also unlocks features to support a variety of use cases without adding complexity. For example, we can use the pending layer to verify that an account will have enough funds after the holds and pending transactions have cleared. With the encumbrance layer, we can add transactions scheduled in the future and goals or budgeting tools to set money aside in an account. Without explicit layers, chaos reigns. Many DIY ledgers that we've seen lack the concept of layers, or only apply the concept partially. This can make it incredibly difficult to reason about the state of a ledger or perform basic accounting operations like cash reconciliation. > **Note:** > > Not every product will need to use all three layers, but they are always available when and if new versions of your product do need to make use of them. ## Layered Balances Each account can have a different aggregate balance in each layer depending on how transactions have been posted. To calculate the account balance for a layer, we sum all entries on that layer: $$ Where $b(l)$ is the balance for a layer and $e_l$ is the set of all entries on that layer. All accounts and account sets a debit balance (sum of debit entries), credit balance (sum of credit entries), and normal balance. Learn more about balances in [Balances](/docs/accounting-core/balances). ## Layers in Practice To demonstrate how layers work in practice, let's consider an account set that has entries posted to its sub-accounts across all three layers. We'll follow the state changes to the account set and its balance as additional entries are written. For simplicity's sake, we'll assume that all amounts are in USD. At first, the only entry is on the settled layer. **Request** ```graphql fragment F_BalanceAmount_Standard on BalanceAmount { normalBalance { formatted(as: { locale: "en-US" }) } drBalance { formatted(as: { locale: "en-US" }) } crBalance { formatted(as: { locale: "en-US" }) } } fragment F_Transaction_Standard on Transaction { transactionId effective tranCode { tranCodeId code version } } # Using fragment: F_AccountSet_Summary query GetBertAccountSummary($set_bertId: UUID!) { accountSet(id: $set_bertId) { accountSetId name balance(currency: "USD") { currency available(layer: ENCUMBRANCE) { ...F_BalanceAmount_Standard } settled { ...F_BalanceAmount_Standard } pending { ...F_BalanceAmount_Standard } encumbrance { ...F_BalanceAmount_Standard } } entries(first: 10) { nodes { direction layer entryType account { accountId code name } amount { units currency formatted(as: { locale: "en-US" }) } transaction { ...F_Transaction_Standard } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "name": "Bert", "balance": { "currency": "USD", "available": { "normalBalance": { "formatted": "$100.00" }, "drBalance": { "formatted": "$100.00" }, "crBalance": { "formatted": "$0.00" } }, "settled": { "normalBalance": { "formatted": "$100.00" }, "drBalance": { "formatted": "$100.00" }, "crBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "$0.00" }, "drBalance": { "formatted": "$0.00" }, "crBalance": { "formatted": "$0.00" } }, "encumbrance": { "normalBalance": { "formatted": "$0.00" }, "drBalance": { "formatted": "$0.00" }, "crBalance": { "formatted": "$0.00" } } }, "entries": { "nodes": [ { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "100", "currency": "USD", "formatted": "$100.00" }, "transaction": { "transactionId": "d52e6593-4973-4522-a065-d6eef9428308", "effective": "2022-10-31", "tranCode": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX", "version": 1 } } } ] } } } } ``` **Variables** ```json { "set_bertId": "65bc724c-6767-4f35-90f9-279a12f95fd4" } ``` Because this entry is on the settled layer, the balance for that layer reflects the amount of that entry. > **Note:** > > The "normal balance" for an account is different for credit normal and debit normal accounts. > > Learn more about [Calculating Normal Balance](/docs/accounting-core/balances#calculating-normal-balance). The pending and encumbrance layers have no entries, so they are currently at $0. Let's see how things change when entries on the pending layer are written. In this case, the entries are modeling a basic "hold-settle" pattern: a hold is placed on the pending layer, and then settled to the settled layer and cleared from the pending layer. **Request** ```graphql fragment F_BalanceAmount_Standard on BalanceAmount { normalBalance { formatted(as: { locale: "en-US" }) } drBalance { formatted(as: { locale: "en-US" }) } crBalance { formatted(as: { locale: "en-US" }) } } fragment F_Transaction_Standard on Transaction { transactionId effective tranCode { tranCodeId code version } } # Using fragment: F_AccountSet_Summary query GetBertAccountSummary($set_bertId: UUID!) { accountSet(id: $set_bertId) { accountSetId name balance(currency: "USD") { currency available(layer: ENCUMBRANCE) { ...F_BalanceAmount_Standard } settled { ...F_BalanceAmount_Standard } pending { ...F_BalanceAmount_Standard } encumbrance { ...F_BalanceAmount_Standard } } entries(first: 10) { nodes { direction layer entryType account { accountId code name } amount { units currency formatted(as: { locale: "en-US" }) } transaction { ...F_Transaction_Standard } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "name": "Bert", "balance": { "currency": "USD", "available": { "normalBalance": { "formatted": "$118.45" }, "drBalance": { "formatted": "$138.95" }, "crBalance": { "formatted": "$20.50" } }, "settled": { "normalBalance": { "formatted": "$118.45" }, "drBalance": { "formatted": "$118.45" }, "crBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "$0.00" }, "drBalance": { "formatted": "$20.50" }, "crBalance": { "formatted": "$20.50" } }, "encumbrance": { "normalBalance": { "formatted": "$0.00" }, "drBalance": { "formatted": "$0.00" }, "crBalance": { "formatted": "$0.00" } } }, "entries": { "nodes": [ { "direction": "CREDIT", "layer": "PENDING", "entryType": "RECORD_SETTLE_PENDING_TX_CR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "20.50", "currency": "USD", "formatted": "$20.50" }, "transaction": { "transactionId": "b04b5df6-e9f7-4358-a353-cb064527bb91", "effective": "2022-11-05", "tranCode": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_SETTLE_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "18.45", "currency": "USD", "formatted": "$18.45" }, "transaction": { "transactionId": "b04b5df6-e9f7-4358-a353-cb064527bb91", "effective": "2022-11-05", "tranCode": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "PENDING", "entryType": "RECORD_PENDING_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "20.50", "currency": "USD", "formatted": "$20.50" }, "transaction": { "transactionId": "f55074da-15d9-5b2d-826f-37c71b9a5db2", "effective": "2022-11-03", "tranCode": { "tranCodeId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "code": "RECORD_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "100", "currency": "USD", "formatted": "$100.00" }, "transaction": { "transactionId": "d52e6593-4973-4522-a065-d6eef9428308", "effective": "2022-10-31", "tranCode": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX", "version": 1 } } } ] } } } } ``` **Variables** ```json { "set_bertId": "65bc724c-6767-4f35-90f9-279a12f95fd4" } ``` Notice that the entries on the pending layer _only affected the pending-layer balance_. The settled balance was only changed by the additional settled-layer entry. Let's see how the balances change with a few more entries across all layers: **Request** ```graphql fragment F_BalanceAmount_Standard on BalanceAmount { normalBalance { formatted(as: { locale: "en-US" }) } drBalance { formatted(as: { locale: "en-US" }) } crBalance { formatted(as: { locale: "en-US" }) } } fragment F_Transaction_Standard on Transaction { transactionId effective tranCode { tranCodeId code version } } # Using fragment: F_AccountSet_Summary query GetBertAccountSummary($set_bertId: UUID!) { accountSet(id: $set_bertId) { accountSetId name balance(currency: "USD") { currency available(layer: ENCUMBRANCE) { ...F_BalanceAmount_Standard } settled { ...F_BalanceAmount_Standard } pending { ...F_BalanceAmount_Standard } encumbrance { ...F_BalanceAmount_Standard } } entries(first: 10) { nodes { direction layer entryType account { accountId code name } amount { units currency formatted(as: { locale: "en-US" }) } transaction { ...F_Transaction_Standard } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "name": "Bert", "balance": { "currency": "USD", "available": { "normalBalance": { "formatted": "$78.45" }, "drBalance": { "formatted": "$183.77" }, "crBalance": { "formatted": "$105.32" } }, "settled": { "normalBalance": { "formatted": "$73.63" }, "drBalance": { "formatted": "$118.45" }, "crBalance": { "formatted": "$44.82" } }, "pending": { "normalBalance": { "formatted": "$0.00" }, "drBalance": { "formatted": "$20.50" }, "crBalance": { "formatted": "$20.50" } }, "encumbrance": { "normalBalance": { "formatted": "$4.82" }, "drBalance": { "formatted": "$44.82" }, "crBalance": { "formatted": "$40.00" } } }, "entries": { "nodes": [ { "direction": "DEBIT", "layer": "ENCUMBRANCE", "entryType": "ASSIGN_TO_BUDGET_DR", "account": { "accountId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "code": "BERT.BUDGET", "name": "Bert's Budget" }, "amount": { "units": "44.82", "currency": "USD", "formatted": "$44.82" }, "transaction": { "transactionId": "95d5b790-d709-4f41-8361-43029b3e1af3", "effective": "2022-11-07", "tranCode": { "tranCodeId": "95ede9f4-b3f6-4cc3-ab18-338fd9f41e8b", "code": "ASSIGN_TO_BUDGET", "version": 1 } } }, { "direction": "CREDIT", "layer": "SETTLED", "entryType": "RECORD_TX_CR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "44.82", "currency": "USD", "formatted": "$44.82" }, "transaction": { "transactionId": "1b76219c-31cc-4776-9fa0-546b6ffe8c27", "effective": "2022-11-07", "tranCode": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX", "version": 1 } } }, { "direction": "CREDIT", "layer": "ENCUMBRANCE", "entryType": "ALLOC_BUDGET_CR", "account": { "accountId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "code": "BERT.BUDGET", "name": "Bert's Budget" }, "amount": { "units": "40", "currency": "USD", "formatted": "$40.00" }, "transaction": { "transactionId": "6403c5e0-9ee9-4ac4-ab2d-b05a2264c3e6", "effective": "2022-11-10", "tranCode": { "tranCodeId": "2e92e3aa-9871-4c47-8c9a-5d76e6340769", "code": "ALLOC_BUDGET", "version": 1 } } }, { "direction": "CREDIT", "layer": "PENDING", "entryType": "RECORD_SETTLE_PENDING_TX_CR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "20.50", "currency": "USD", "formatted": "$20.50" }, "transaction": { "transactionId": "b04b5df6-e9f7-4358-a353-cb064527bb91", "effective": "2022-11-05", "tranCode": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_SETTLE_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "18.45", "currency": "USD", "formatted": "$18.45" }, "transaction": { "transactionId": "b04b5df6-e9f7-4358-a353-cb064527bb91", "effective": "2022-11-05", "tranCode": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "PENDING", "entryType": "RECORD_PENDING_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "20.50", "currency": "USD", "formatted": "$20.50" }, "transaction": { "transactionId": "f55074da-15d9-5b2d-826f-37c71b9a5db2", "effective": "2022-11-03", "tranCode": { "tranCodeId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "code": "RECORD_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "100", "currency": "USD", "formatted": "$100.00" }, "transaction": { "transactionId": "d52e6593-4973-4522-a065-d6eef9428308", "effective": "2022-10-31", "tranCode": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX", "version": 1 } } } ] } } } } ``` **Variables** ```json { "set_bertId": "65bc724c-6767-4f35-90f9-279a12f95fd4" } ``` Again, notice that the **layered balances** are only aggregating entries from their layer. ## Calculating an Available Balance While keeping separate balances for each layer is useful for tracking the current, planned, and predicted state of accounts, it is also possible to calculate a balance across layers. The **available** balance is a special balance that rolls up the balances of the other layer balances. If we show the available balance from the example above, this totaling is clearly visible. **Request** ```graphql fragment F_BalanceAmount_Standard on BalanceAmount { normalBalance { formatted(as: { locale: "en-US" }) } drBalance { formatted(as: { locale: "en-US" }) } crBalance { formatted(as: { locale: "en-US" }) } } fragment F_Transaction_Standard on Transaction { transactionId effective tranCode { tranCodeId code version } } # Using fragment: F_AccountSet_Summary query GetBertAccountSummary($set_bertId: UUID!) { accountSet(id: $set_bertId) { accountSetId name balance(currency: "USD") { currency available(layer: ENCUMBRANCE) { ...F_BalanceAmount_Standard } settled { ...F_BalanceAmount_Standard } pending { ...F_BalanceAmount_Standard } encumbrance { ...F_BalanceAmount_Standard } } entries(first: 10) { nodes { direction layer entryType account { accountId code name } amount { units currency formatted(as: { locale: "en-US" }) } transaction { ...F_Transaction_Standard } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "name": "Bert", "balance": { "currency": "USD", "available": { "normalBalance": { "formatted": "$78.45" }, "drBalance": { "formatted": "$183.77" }, "crBalance": { "formatted": "$105.32" } }, "settled": { "normalBalance": { "formatted": "$73.63" }, "drBalance": { "formatted": "$118.45" }, "crBalance": { "formatted": "$44.82" } }, "pending": { "normalBalance": { "formatted": "$0.00" }, "drBalance": { "formatted": "$20.50" }, "crBalance": { "formatted": "$20.50" } }, "encumbrance": { "normalBalance": { "formatted": "$4.82" }, "drBalance": { "formatted": "$44.82" }, "crBalance": { "formatted": "$40.00" } } }, "entries": { "nodes": [ { "direction": "DEBIT", "layer": "ENCUMBRANCE", "entryType": "ASSIGN_TO_BUDGET_DR", "account": { "accountId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "code": "BERT.BUDGET", "name": "Bert's Budget" }, "amount": { "units": "44.82", "currency": "USD", "formatted": "$44.82" }, "transaction": { "transactionId": "95d5b790-d709-4f41-8361-43029b3e1af3", "effective": "2022-11-07", "tranCode": { "tranCodeId": "95ede9f4-b3f6-4cc3-ab18-338fd9f41e8b", "code": "ASSIGN_TO_BUDGET", "version": 1 } } }, { "direction": "CREDIT", "layer": "SETTLED", "entryType": "RECORD_TX_CR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "44.82", "currency": "USD", "formatted": "$44.82" }, "transaction": { "transactionId": "1b76219c-31cc-4776-9fa0-546b6ffe8c27", "effective": "2022-11-07", "tranCode": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX", "version": 1 } } }, { "direction": "CREDIT", "layer": "ENCUMBRANCE", "entryType": "ALLOC_BUDGET_CR", "account": { "accountId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "code": "BERT.BUDGET", "name": "Bert's Budget" }, "amount": { "units": "40", "currency": "USD", "formatted": "$40.00" }, "transaction": { "transactionId": "6403c5e0-9ee9-4ac4-ab2d-b05a2264c3e6", "effective": "2022-11-10", "tranCode": { "tranCodeId": "2e92e3aa-9871-4c47-8c9a-5d76e6340769", "code": "ALLOC_BUDGET", "version": 1 } } }, { "direction": "CREDIT", "layer": "PENDING", "entryType": "RECORD_SETTLE_PENDING_TX_CR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "20.50", "currency": "USD", "formatted": "$20.50" }, "transaction": { "transactionId": "b04b5df6-e9f7-4358-a353-cb064527bb91", "effective": "2022-11-05", "tranCode": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_SETTLE_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "18.45", "currency": "USD", "formatted": "$18.45" }, "transaction": { "transactionId": "b04b5df6-e9f7-4358-a353-cb064527bb91", "effective": "2022-11-05", "tranCode": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "PENDING", "entryType": "RECORD_PENDING_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "20.50", "currency": "USD", "formatted": "$20.50" }, "transaction": { "transactionId": "f55074da-15d9-5b2d-826f-37c71b9a5db2", "effective": "2022-11-03", "tranCode": { "tranCodeId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "code": "RECORD_PENDING_TX", "version": 1 } } }, { "direction": "DEBIT", "layer": "SETTLED", "entryType": "RECORD_TX_DR", "account": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "amount": { "units": "100", "currency": "USD", "formatted": "$100.00" }, "transaction": { "transactionId": "d52e6593-4973-4522-a065-d6eef9428308", "effective": "2022-10-31", "tranCode": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX", "version": 1 } } } ] } } } } ``` **Variables** ```json { "set_bertId": "65bc724c-6767-4f35-90f9-279a12f95fd4" } ``` We can query the available balance with a configuration option to roll up balances: * across all layers, * over the settled and pending layers, * or just the settled layer (at which point it is equivalent to the settled balance). One way to think of the available balance is as a function that takes a layer and sums some or all of the other layer balances depending on which layer is provided. a(l)= \\begin{dcases} b(S),& \\text{if } l=S\\\\ b(S) + b(P),& \\text{if } l=P\\\\ b(S) + b(P) + b(E),& \\text{if } l=E\\\\ \\end{dcases} $$ Where $a(l)$ is the available balance at a given layer, $b(l)$ is the balance for a layer, and $S, P, E$ represent the Settled, Pending, and Encumbrance layers. --- # Ledgers in Twisp The accounting core is built upon a single source-of-truth ledger. Source: https://www.twisp.com/docs/accounting-core/ledgers-in-twisp ## What is a Ledger? Financial ledgers are the foundation of systems that track money. Double-entry accounting has been the de facto mechanism to record financial transactions for over 500 years, and ledgers are at the heart of this system. - Banks track debits and credits across many accounts, with multiple financial instruments, all the while analyzing this data to surface financial insights to consumers. - Payment infrastructure companies need robust multi-tenant account servicing built on many FBOs, made available to many clients via a secure API. - Accounting software depends on strong transactional guarantees and zero-downtime data access to ensure accuracy and reliability. Financial ledgers and the data derived from them are the engine of all such systems. ## Ledgers in Twisp At Twisp, we set out to rethink the underlying technology for financial ledger systems by combining the operational and scaling characteristics of a distributed database with the correctness guarantees offered by relational databases. > **Note:** > > To learn more about the infastructure that powers our ledgers, read the [Infrastructure](/docs/infrastructure) section. The ledger for our accounting core is designed to be flexible enough to support any financial product, yet structured enough to provide a reliable and trustworthy store of financial data. ## Architecture With your Twisp account, you can provision multiple instances of the accounting core. Each instance is powered by a single ledger. A ledger is composed of journals, accounts, entries, transactions, and balances. ### Journals Journals allow for the organizing of transactions within separate "books". In many cases, users only need a single journal. For this reason, Twisp always contains a default journal with code `DEFAULT`. Journals can be used for a variety of functions. For example, users may create separate journals for different currencies, or product-specific journals. ### Accounts A chart of accounts models all of the economic activity that your ledger provides. The chart of accounts is the basis for creating balance sheets, P&L reports, and for understanding the balances for the customer and business entities your business services. Read more about accounts in [Chart of Accounts](/docs/accounting-core/chart-of-accounts). ### Layered Entries An entry represents one side of a transaction in a ledger. In other systems, these may be called "ledger lines" or "journal entries". Twisp enforces double-entry accounting, which in practice means that entries can only be entered in the context of a Transaction. Posting a transaction will create _at least 2_ ledger entries. Entries always have an account, amount, direction (CREDIT or DEBIT), and type for denoting an particular entry category (e.g. "TRANSFER_DR" for an entry representing the debit side of a transfer). In addition, every entry is assigned to a layer (SETTLED, PENDING, and ENCUMBRANCE) to differentiate between entries in various stages of a transaction lifecycle. Read more about how layers work in [Layered Accounting](/docs/accounting-core/layered-accounting). ### Transactions Transactions record all accounting events in the ledger. In Twisp, the only way to write to a ledger is through a transaction and every transaction writes two or more entries to the ledger in standard double-entry accounting practice. Twisp expands upon the basic principle of an accounting transaction with additional features like transaction codes and correlations. Transaction Codes (tran codes) are how financial engineers do double-entry accounting: they encode the basic patterns for a type of transaction as a predictable and repeatable formula. Read more about the power of transactions and tran codes in [Encoded Transactions](/docs/accounting-core/encoded-transactions). ### Balances Balances are auto-calculated sums of the entries for a given account. Each account can have balances across all three layers, and every balance record maintains a separate debit and credit balance amount. Read more about how balances are calculated in [Balances](/docs/accounting-core/balances). ## Architected from Principles Our ledger has been designed in adherence to strong, time-tested accounting principles. - **Enforce double-entry principles**.\ In other words, ensure that value (money) is never created or destroyed. There is always a debit and credit side for every transaction. - **History must be fully preserved.**\ Ledgers are immutable and append-only. There is always a full history of changes to provide a clear, unbroken lineage of the current state. - **Contextualize every transaction**.\ Much financial activity happens across multiple phases of a lifecycle, with different transactions at each phase. Proper use of correlations, layers, transaction metadata means that no useful context is lost across that lifecycle. - **Design for composability**.\ With transaction codes (tran codes) and automations, higher-order accounting logic can be encoded into a simplified interface. This way, your designers and engineers can focus on the business logic of your product, not the accounting details. - **When in doubt, rely upon industry standards**.\ There are times to be inventive, and times to trust the system. We've learned from experience and research to make use of well-established accounting patterns like layers and typed entries. --- # Building Financial Systems with the Twisp Accounting Core Design, implement, and optimize a financial system using Twisp. Covers everything from defining the financial services to be provided, designing accounts and transactions, ensuring security and compliance, integrating with other systems, testing for accuracy, and planning for scalability. The ultimate goal is to create a robust, secure, and efficient financial system capable of handling growing transaction volumes. Source: https://www.twisp.com/docs/guides/building-financial-systems ## Defining financial services In this section, we'll guide you through the process of identifying the financial services your system will provide and determine the specific requirements for each service. ### 1. Identify the financial services your system will provide Start by defining the financial products and services your system will offer, such as checking accounts, savings accounts, loans, and credit cards. ### 2. Determine the specific requirements for each service Once you have identified the financial services your system will provide, you need to determine the specific requirements for each service. This includes account types, transaction types, and fees associated with each service. A hypothetical [neobank](https://www.nerdwallet.com/article/banking/what-is-a-neobank), for example, might provide the following services: - **Checking Accounts**: These accounts will support deposits, withdrawals, and direct transfers between customers. You'll need to create a chart of accounts that includes customer checking accounts, as well as internal company accounts for tracking revenue and expenses. - **Savings Accounts**: Similar to checking accounts, these accounts will also support deposits, withdrawals, and transfers. You may also want to include interest calculations and fees for certain actions, such as withdrawing funds before a specified period. - **Loans**: For loan products, you'll need to track the principal balance, interest rate, and repayment schedule. You'll also need to create accounts to represent loan disbursements and repayments. With Twisp, you can create a flexible chart of accounts to model the economic activities required for your specific use case. Additionally, Twisp enables you to define transaction codes to represent different types of financial activities, such as deposits, withdrawals, transfers, and fees. This will help you design a robust and adaptable system to support the financial services you offer. In the next section, we will delve into designing accounts and transactions to support these financial services using Twisp's accounting core features. ## Designing accounts and transactions In this section, we will walk you through the steps to design and create a [chart of accounts](/docs/accounting-core/chart-of-accounts) and [transaction codes](/docs/tutorials/advanced/designing-tran-codes) for different types of financial activities. This process is crucial for organizing your financial data and establishing rules for posting transactions to accounts. ### 1. Create a chart of accounts A chart of accounts models all the economic activity that your financial system will provide. It is the basis for creating balance sheets, P&L reports, and for understanding the balances for the customer and business entities your system services. To create a chart of accounts, you need to: 1. Identify the different account types you need to support your financial services. These can include assets, liabilities, income, and expenses. 2. Create accounts for each type and purpose. For example, you might have accounts for customer wallets, settlement accounts for ACH transactions, and accounts for revenue. 3. Organize accounts into sets using the [AccountSet](/docs/reference/graphql/types/object#account-set) type. This will help you manage hierarchical tree structures and roll up balances across multiple accounts. > **Note:** > > For more on how to set up a chart of accounts, see the tutorials on [Setting Up Accounts](/docs/tutorials/setting-up-accounts) and [Organizing With Account Sets](/docs/tutorials/organizing-with-account-sets). ### 2. Define transaction codes Transaction codes (tran codes) represent different types of financial activity, such as deposits, withdrawals, transfers, and fees. They encode the basic patterns for a type of transaction as a predictable and repeatable formula. To define transaction codes, you need to: 1. Determine the type of transaction (e.g., ACH transfer, credit card purchase, foreign exchange). 2. Identify the accounts involved in the transaction. Determine where the money is coming from and where it is going, including any additional accounts that need to be debited or credited. 3. Establish the number of entries that should be written to the ledger for each transaction, as well as which entries go on the debit side and which on the credit side. 4. Specify whether these entries reflect a settled amount, or if they are still pending a final settlement. > **Note:** > > For more on how to work with tran codes, see the tutorials on [Building Tran Codes](/docs/tutorials/building-tran-codes) and [Designing Tran Codes](/docs/tutorials/advanced/designing-tran-codes). ### 3. Establish rules for posting transactions Once you have designed your accounts and transaction codes, you need to establish rules for posting transactions to accounts based on the transaction codes. This process involves: 1. Creating a framework for how transactions are recorded in the ledger. In Twisp, every transaction writes two or more entries to the ledger in standard double-entry accounting practice. 2. Ensuring that your transaction codes and accounts align with the double-entry accounting principles. For every transaction, there should be at least two entries that balance out across debits and credits. 3. Implementing the rules in your financial system to automatically post transactions to the appropriate accounts based on the transaction codes. By following these steps, you can successfully design accounts and transactions to support your financial system built on the Twisp accounting core. ## Security and compliance Ensuring the security and compliance of your financial system is critical to protect sensitive data, maintain customer trust, and meet industry regulations. With Twisp, you have access to a secure, reliable, and scalable foundation to build your financial system. ### Meet industry standards for security and data protection Twisp's Financial Ledger Database (FLDB) is designed to provide strong guarantees about the lineage of data, ensuring the integrity, transparency, and accuracy of your financial data. Additionally, the Twisp Accounting Core is architected with adherence to strong, time-tested accounting principles, such as the enforcement of double-entry principles, immutable and append-only ledgers, and proper transaction contextualization. ### Implement strong access controls and user authentication mechanisms Twisp's infrastructure includes explicit authorization policies and dedicated security resources, ensuring that access controls are centralized and well-defined. By managing authentication and authorization centrally, you can more easily maintain a secure environment and minimize the risk of unauthorized access or data breaches. ### Configure audit trails to track changes and maintain a history of transactions and account activity The Twisp Accounting Core provides built-in mechanisms for timing and sequencing, which is essential for accurate auditing and reconciliation of financial data. By maintaining a complete, unbroken lineage of transactions and account activity, you can easily monitor your system's performance, usage, and compliance with industry regulations. By leveraging the security features of Twisp's infrastructure and implementing appropriate access controls and audit trails, you can ensure that your financial system is secure, compliant, and ready for use in a modern financial product. > **Note:** > > Read more about security and authentication in Twisp: [Security And Auth](/docs/infrastructure/security-and-auth). ## Integrations with Twisp accounting core Integrating Twisp with other financial systems and business tools is essential for streamlining data flow, automating processes, and building a seamless financial ecosystem. In this section, we will discuss how to integrate Twisp with payment processors, banking APIs, CRMs, and ERPs. ### 1. Payment processors and banking APIs To integrate Twisp with payment processors and banking APIs, you can use the [GraphQL API](/docs/reference/graphql). The API allows you to interact with the Twisp Accounting Core, manage transactions, and synchronize data with external financial systems. Here's how: 1. Identify the payment processor or banking API you want to integrate with and gather the necessary API credentials. 2. Develop a custom integration layer to handle communication between Twisp and the external system. This can involve sending transactions to the payment processor, receiving transaction updates, and synchronizing account balances. 3. Use the Twisp GraphQL API to [post transactions](/docs/reference/graphql/mutations#post-transaction), [query account balances](/docs/reference/graphql/queries#balances), and manage other financial data in your Twisp ledger. Ensure that your integration layer updates the Twisp ledger with any changes or transactions from the external system. ### 2. CRMs and ERPs Integrating Twisp with Customer Relationship Management (CRM) and Enterprise Resource Planning (ERP) systems can help streamline data flow and automate processes. Here's how to integrate Twisp with these business tools: 1. Identify the CRM or ERP system you want to integrate with, and gather the necessary API credentials. 2. Develop a custom integration layer to handle communication between Twisp and the CRM/ERP system. This can involve synchronizing financial data, such as transaction records, account balances, and customer information, between the systems. 3. Use the Twisp GraphQL API to interact with the Twisp Accounting Core and manage financial data, such as posting transactions, querying account balances, and updating customer information. Ensure that your integration layer maintains data consistency between Twisp and the CRM/ERP system. Following these steps will ensure a seamless integration between Twisp Accounting Core and other financial systems and business tools. By connecting Twisp with payment processors, banking APIs, CRMs, and ERPs, you can build a robust financial ecosystem that streamlines data flow, automates processes, and delivers a comprehensive solution for managing your financial operations. ## Testing and validation An essential step in building a financial system with Twisp Accounting Core is to test and validate the functionality, performance, and accuracy of the ledger and its calculations. By performing comprehensive tests and validation checks, you can ensure that your financial system behaves as expected, and that the data it generates is reliable. In this section, we will walk through the testing and validation process for your Twisp-based financial system. ### 1. Test with sample data Begin by populating your Twisp ledger with sample data that mimics the financial activity and transactions you expect to see in your final product. Use the GraphQL API to create accounts, define transaction codes, and post transactions that cover a variety of use cases. For example, in a hypothetical neobank you would create sample customer accounts for checking, savings, and loans, then post transactions such as deposits, withdrawals, transfers, and fees. By working with realistic sample data, you can identify potential issues and areas for improvement early in the development process. ### 2. Test various use cases Ensure that your financial system can handle a wide range of scenarios by testing multiple use cases. For instance, test the ledger's handling of edge cases, such as large transactions, insufficient funds, and high transaction volumes. Additionally, test the system's response to invalid or incorrect data, such as incomplete transaction details or incorrect account numbers. This could involve simulating a large transfer between customer accounts, an attempt to withdraw more funds than a customer has available, or processing a high number of transactions within a short period. ### 3. Validate calculations and balances As part of the testing process, it is crucial to verify that the balances reflect the intent of your account structures. To do this, compare the results produced by your financial system with manually calculated or known results. You might check that the balances for each sample customer account are updated correctly after each transaction and that any applicable fees are calculated and applied accurately. ### 4. Assess performance and scalability Finally, test your financial system's performance and ability to scale under increasing data and transaction volumes. Twisp's infrastructure is designed to provide high performance and support massively multi-tenant workloads. Still, it is essential to confirm that your specific configuration can handle your anticipated transaction volume and growth. To test performance and scalability, you could simulate a large influx of new customer accounts and a high volume of transactions over a short period. Monitor response times, processing speeds, and overall system performance to identify any potential bottlenecks or areas for optimization. By following these steps, you can ensure that your Twisp-based financial system functions correctly, provides accurate data, and performs well under varying conditions. This thorough testing and validation process will give you the confidence needed when working with mission-critical financial data and help you create a reliable and robust financial product. ## Conclusion Building financial systems with the Twisp Accounting Core offers a comprehensive approach to handling the complexities of modern financial operations. This guide has outlined the critical steps in this process, from identifying the financial services your system will provide and designing corresponding accounts and transactions, to enforcing strong security measures and ensuring compliance. We also discussed the importance of effective integration with other financial systems and business tools to streamline processes, reduce manual intervention, and enhance overall efficiency. The value of thorough testing to ensure the system functions as expected, producing reliable and accurate data, cannot be overstated. In essence, creating a robust, secure, and efficient financial system is a complex, yet feasible task with Twisp Accounting Core. By following the steps outlined in this guide, you'll be well on your way to developing a financial system that not only meets your current needs but is also equipped to handle future growth and changes in your financial landscape. Harness the power of Twisp and embrace the confidence that comes with a solid, reliable, and flexible financial system. --- # Effective Balances Roll up balances by the effective date of activity to answer point-in-time, statement, and income-statement questions — even when transactions arrive late or are backdated. Source: https://www.twisp.com/docs/guides/effective-balances Standard balances answer the question _"what is this account worth right now?"_. They are a single running total that always reflects the latest state of the ledger. Effective balances answer a different question: _"what was true **as of**, or **during**, a particular period?"_ — for example _"what was this account's balance at the end of January?"_ or _"how much did merchant `M001` deposit in Q1?"_. Twisp builds effective balances by **rolling up a calculation's dimensions by the effective date of each entry** into year, month, and day buckets. Because every entry carries an effective date, these rollups are maintained on write, so you can query a historical or period-scoped balance with the same certainty and latency as a live balance. > **Task:** > > - Enable effective balances on a calculation with `config.enableEffectiveBalances` > - Query a single period (`YYYY`, `YYYY-MM`, or `YYYY-MM-DD`) with `effective.period` > - Aggregate a span of periods with `effective.range` > - Get a cumulative "as-of" balance with `effective.cumulative` > - Walk per-period activity or running balances with `effective.periods` and `accumulate` > - Control which date drives the rollup with `effectiveDateSource` --- ## How effective balances work Effective balances are an extension of [calculations](/docs/accounting-core/balances) — the same compute-on-write rollups that produce dimensional balances. When you set `enableEffectiveBalances: true` on a calculation, Twisp creates **three child calculations** behind your parent calculation, one for each granularity: | Granularity | Period format | Example | | ----------- | ------------- | ------------ | | Year | `YYYY` | `2024` | | Month | `YYYY-MM` | `2024-02` | | Day | `YYYY-MM-DD` | `2024-02-15` | The effective date components (`year`, `month`, `day`) are **prepended to your custom dimensions**. So a calculation that already dimensions by `merchant_id` and `category` will, with effective balances enabled, maintain buckets keyed by `(year, merchant_id, category)`, `(year, month, merchant_id, category)`, and `(year, month, day, merchant_id, category)`. At query time you supply the parent `calculationId` and an `effective` argument; Twisp resolves the correct child calculation from the period format and merges the effective-date dimensions with the custom dimensions you pass. > **Note:** > > `year`, `month`, and `day` are **reserved** dimension names once effective balances are enabled. You cannot use them as custom dimension aliases, and you cannot pass them in the `dimension` argument — Twisp populates them for you from the effective date. ## Setting up an effective calculation Enable effective balances on a calculation by setting `config.enableEffectiveBalances`. Custom dimensions are defined as usual with CEL expressions over the entry: ```graphql mutation CreateEffectiveCalculation { createCalculation( input: { calculationId: "22222222-2222-2222-2222-222222222222" code: "merchant_balances" description: "Balances by merchant with effective date rollups" dimensions: [ { alias: "merchant_id" value: "string(context.vars.entry.metadata.?merchant_id.orValue(''))" } { alias: "category", value: "string(context.vars.entry.metadata.?category.orValue(''))" } ] condition: "context.vars.entry.metadata != null && ('merchant_id' in context.vars.entry.metadata) && ('category' in context.vars.entry.metadata)" scope: GLOBAL config: { enableEffectiveBalances: true } } ) { calculationId code config { enableEffectiveBalances } dimensions { alias value } } } ``` You can also enable effective balances on a calculation with **no custom dimensions** — the child calculations are then keyed only by the effective-date components, giving you plain time-bucketed balances for the account. > **Note:** > > A calculation must have `enableEffectiveBalances: true` to be queried with an `effective` argument. Querying a regular calculation with `effective` returns an error. Once the calculation exists, post your normal dated activity. The examples below assume deposits to a single account across three months, tagged with merchant and category metadata: | Effective date | Merchant | Category | Amount | | -------------- | -------- | -------- | ------- | | `2024-01-15` | M001 | FOOD | 1000.00 | | `2024-02-15` | M001 | FOOD | 2000.00 | | `2024-03-15` | M001 | FOOD | 3000.00 | | `2024-01-20` | M001 | TRAVEL | 500.00 | | `2024-02-20` | M001 | TRAVEL | 1000.00 | | `2024-01-25` | M002 | FOOD | 750.00 | ## Querying a single period Pass `effective.period` to read the balance for one period. Twisp infers the granularity from the format — `YYYY` for a year, `YYYY-MM` for a month, `YYYY-MM-DD` for a day — and selects the matching child calculation. The `dimensions` field in the response echoes back the resolved bucket, including the effective-date components. ```graphql query QueryEffectiveBalancePeriod { year: balance( accountId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "22222222-2222-2222-2222-222222222222" dimension: { merchant_id: "M001", category: "FOOD" } effective: { period: "2024" } ) { available(layer: SETTLED) { crBalance { units } } calculationId dimensions } month: balance( accountId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "22222222-2222-2222-2222-222222222222" dimension: { merchant_id: "M001", category: "FOOD" } effective: { period: "2024-02" } ) { available(layer: SETTLED) { crBalance { units } } calculationId dimensions } day: balance( accountId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "22222222-2222-2222-2222-222222222222" dimension: { merchant_id: "M001", category: "FOOD" } effective: { period: "2024-02-15" } ) { available(layer: SETTLED) { crBalance { units } } calculationId dimensions } } ``` The year bucket totals all three FOOD deposits (`6000.00`); the month and day buckets isolate February's activity (`2000.00`). Note how the resolved `calculationId` differs per granularity — these are the child calculations — and how `dimensions` reports the effective-date components: ```json { "data": { "year": { "available": { "crBalance": { "units": "6000.00" } }, "calculationId": "25c6d76d-9e22-5726-8e13-53bfe3f60e88", "dimensions": { "category": "FOOD", "merchant_id": "M001", "year": 2024 } }, "month": { "available": { "crBalance": { "units": "2000.00" } }, "calculationId": "61d544eb-7a1a-5fdc-8494-a89b795aab75", "dimensions": { "category": "FOOD", "merchant_id": "M001", "month": 2, "year": 2024 } }, "day": { "available": { "crBalance": { "units": "2000.00" } }, "calculationId": "8b01e31e-5df4-5e4e-a68c-1f3e0f1db826", "dimensions": { "category": "FOOD", "day": 15, "merchant_id": "M001", "month": 2, "year": 2024 } } } } ``` ## Aggregating a range of periods `effective.range` sums activity across a span of periods given a closed `gte`/`lte` pair. This is convenient for income-statement-style totals over a window. ```graphql query QueryEffectiveBalanceRange { range: balance( accountId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "22222222-2222-2222-2222-222222222222" dimension: { merchant_id: "M001", category: "FOOD" } effective: { range: { gte: "2024-01", lte: "2024-03" } } ) { available(layer: SETTLED) { crBalance { units } } calculationId dimensions } } ``` ```json { "data": { "range": { "available": { "crBalance": { "units": "3000.00" } }, "calculationId": "22222222-2222-2222-2222-222222222222", "dimensions": { "category": "FOOD", "merchant_id": "M001" } } } } ``` > **Warning:** > > `range` resolves each bound to the **first day of its period**, so a month- or year-granularity `lte` does not include that whole period. In the example above the window is `[2024-01-01, 2024-03-01]`, which fully contains January and February (`1000.00 + 2000.00 = 3000.00`) but only the first day of March — so the March 15th deposit is excluded. > > For an unambiguous month or year span, prefer `effective.periods` (below), whose half-open `[gte, lt)` interval makes the boundary explicit, or express `range` bounds at day granularity (`YYYY-MM-DD`). ## Cumulative "as-of" balances `effective.cumulative` returns the balance accumulated from the beginning of the ledger through the given date — the answer to _"what was the balance as of this date?"_. ```graphql query QueryEffectiveBalanceCumulative { cumulative: balance( accountId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "22222222-2222-2222-2222-222222222222" dimension: { merchant_id: "M001", category: "FOOD" } effective: { cumulative: "2024-03-31" } ) { available(layer: SETTLED) { crBalance { units } } calculationId dimensions } } ``` Through March 31st, all three FOOD deposits are included: ```json { "data": { "cumulative": { "available": { "crBalance": { "units": "6000.00" } }, "calculationId": "22222222-2222-2222-2222-222222222222", "dimensions": { "category": "FOOD", "merchant_id": "M001" } } } } ``` ## Per-period and running balances `effective.periods` enumerates every period in the half-open interval `[gte, lt)` and returns one entry per period in the `effectiveBalances` array. Granularity is inferred from the bound format, both bounds must use the same format, and `lt` must be strictly greater than `gte`. The `accumulate` flag controls what each entry — and the top-level balance — represents: - **`accumulate: false`** (default): each `effectiveBalances` entry is that period's **own activity**, and the top-level balance is the **sum** across the range. Useful for income statements and per-period breakdowns. - **`accumulate: true`**: each entry is the **running cumulative balance through the end of its period**, and the top-level balance is the **closing cumulative just before `lt`**. Useful for daily statement snapshots and "as-of" series. ```graphql query QueryEffectiveBalancePeriods { periods: balance( accountId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "22222222-2222-2222-2222-222222222222" dimension: { merchant_id: "M001", category: "FOOD" } effective: { periods: { gte: "2024-01", lt: "2024-04", accumulate: true } } ) { available(layer: SETTLED) { crBalance { units } } calculationId dimensions effectiveBalances { effective dimensions available(layer: SETTLED) { crBalance { units } } } } } ``` With `accumulate: true`, each monthly entry carries the running total — January `1000.00`, February `3000.00`, March `6000.00` — and the top-level balance is the closing cumulative (`6000.00`): ```json { "data": { "periods": { "available": { "crBalance": { "units": "6000.00" } }, "calculationId": "22222222-2222-2222-2222-222222222222", "dimensions": { "category": "FOOD", "merchant_id": "M001" }, "effectiveBalances": [ { "effective": "2024-01", "dimensions": { "category": "FOOD", "merchant_id": "M001", "year": "2024", "month": "1" }, "available": { "crBalance": { "units": "1000.00" } } }, { "effective": "2024-02", "dimensions": { "category": "FOOD", "merchant_id": "M001", "year": "2024", "month": "2" }, "available": { "crBalance": { "units": "3000.00" } } }, { "effective": "2024-03", "dimensions": { "category": "FOOD", "merchant_id": "M001", "year": "2024", "month": "3" }, "available": { "crBalance": { "units": "6000.00" } } } ] } } } ``` Run the same query with `accumulate: false` to instead get each month's standalone activity (`1000.00`, `2000.00`, `3000.00`) with a top-level total of `6000.00`. The opening balance for a range can be recovered by querying the prior period with `cumulative`, or by subtracting the first entry's activity from its accumulated value. ## Controlling the effective date with `effectiveDateSource` By default, the effective-date buckets are derived from the transaction's effective date (`context.vars.transaction.effective`). Sometimes you want the rollup to follow a **different** date — most commonly a statement or posting date that differs from when the activity actually occurred. Set `config.effectiveDateSource` to a CEL expression resolving to a date. This is what makes effective balances robust to **backdated and late-arriving activity**: you decide which date the balance "belongs" to. A common pattern is to read a date from entry metadata and fall back to the transaction effective date when it is absent: ```graphql config: { enableEffectiveBalances: true effectiveDateSource: "date(context.vars.entry.?metadata.?statementDate.orValue(context.vars.transaction.effective))" } ``` For example, suppose two deposits occur on `2024-01-15` and `2024-02-20`, but both belong to a statement dated `2024-06-01` (carried on each entry as a `statementDate` metadata value): | Transaction effective | Statement date | Amount | | --------------------- | -------------- | ------- | | `2024-01-15` | `2024-06-01` | 1000.00 | | `2024-02-20` | `2024-06-01` | 500.00 | With `effectiveDateSource` pointed at the statement date, the rollup buckets use **June 2024**, not the transaction effective dates. Querying the statement period returns the full `1500.00`: ```graphql query QueryStatementDateBalances { month: balance( accountId: "bb001111-2222-3333-4444-555566667777" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "aaaa1111-bbbb-cccc-dddd-eeee2222ffff" dimension: {} effective: { period: "2024-06" } ) { available(layer: SETTLED) { crBalance { units } } dimensions } } ``` ```json { "data": { "month": { "available": { "crBalance": { "units": "1500.00" } }, "dimensions": { "month": 6, "year": 2024 } } } } ``` Querying the original **transaction** effective period (`2024-01`) now returns `null` — no buckets were created there, because `effectiveDateSource` redirected every entry to June: ```graphql query QueryTransactionEffectiveEmpty { jan: balance( accountId: "bb001111-2222-3333-4444-555566667777" journalId: "11111111-1111-1111-1111-111111111111" calculationId: "aaaa1111-bbbb-cccc-dddd-eeee2222ffff" dimension: {} effective: { period: "2024-01" } ) { available(layer: SETTLED) { crBalance { units } } } } ``` ```json { "data": { "jan": null } } ``` ## Practical applications Effective balances are the building block for any reporting that is anchored to _when activity is effective_ rather than _when it was recorded_: - **Backdated and late-arriving transactions.** A correction posted today with an effective date of last month lands in last month's bucket automatically. The historical period balance updates on write — no re-run or batch job — so reports stay correct even as adjustments trickle in. - **Monthly statements.** Use `effectiveDateSource` to bucket activity by statement date, then read the statement period with `effective.period`, or use `effective.periods` with `accumulate: true` to produce a running daily balance for the statement. - **Income statements.** Sum a window of activity with `effective.range`, or break it out period-by-period with `effective.periods` and `accumulate: false`. - **As-of / point-in-time balances.** `effective.cumulative` answers "what was the balance as of date X" for audits, reconciliations, and regulatory snapshots. - **Dimensional time series.** Combine effective dates with your own dimensions (merchant, category, product) to get per-period totals sliced any way your calculation defines. > **Note:** > > Effective balances compose with the rest of the balance API — pick a `layer` (`SETTLED`, `PENDING`, `ENCUMBRANCE`), read `available`, and use the same `crBalance` / `drBalance` / `normalBalance` amounts described in [Balances](/docs/accounting-core/balances) and [Pulling Balances](/docs/tutorials/pulling-balances). --- # Handling ACH Returns Process and manage ACH returns effectively Source: https://www.twisp.com/docs/guides/handling-ach-returns ## Coming Soon --- # Guides Use these how-to guides to build solutions for real-world financial systems using Twisp. Source: https://www.twisp.com/docs/guides --- # Modeling Banking on Twisp Design and Implement bank-like core accounting on Twisp. Source: https://www.twisp.com/docs/guides/neobanking-guide ## Context Organizations that offer financial products as part of their core product offering have a wide variety of service providers to bring products to market. There are vertical banking integrations such as Unit, which provide turn key treasury, lending and card operations. Other companies are more specialized, card processors like Marqeta and Lithic and ACH and payments companies like Sila and Stripe. As a company building on top of these service providers you're often going through a "Crawl, Walk, Run" maturation cycle: ```mermaid timeline title Maturation Cycle of Fintech Crawl : Lean on BaaS providers : Shallow banking relationships : Use case limited Walk : Use specialized processors : Deep banking relationships : Innovation unlocked Run : In house capability : Becoming a bank feasible : Highest leverage ``` The goal as a fintech is to prove your product and get to the "Run" stage as fast as possible. One of the first systems you'll need to build and operate well is a core account system. Twisp is a core accounting system designed to provide a system of record for organizations that build financial products and services. In this document we'll dive into how to start modeling deposit accounts, credit accounts and their interaction with various payment instruments, which you'll find applicable to any stage of development. ## Scope In this document we'll cover: 1. Deposit and Credit accounts 3. Card transactions 4. ACH We will design a number of "system level" accounts for operational and double entry accounting purposes. And we'll build out sample chart of accounts for a business neobanking vertical that we can iterate on toward your use case. ## Chart of Accounts The chart of accounts in your fintech system is the building block for how you want balances to "roll up" for both end users and for your platform. It is helpful to think of these charts as separate ones that interact with each other when funds are spent: 1. **End User**: Track end user activity and control how balances "roll up" for end users of our system. 2. **Platform**: Track settlements, revenue and operational concerns of the platform. ### End User We're going to cover two basic kinds of accounts: - **Deposit Accounts**: DDA accounts are stores of value for banking customers. Debit instruments are hooked up to these accounts and these accounts are considered liabilities to the platform, because the platform owes the deposits to the account holders. - **Credit Accounts**: Credit accounts are accounts with a line of credit (as we'll see later on, literally a line on an account) coupled with a payable account. Customers will spend money and then they owe the account issuer funds back by a certain date, and the payable account accrues interest. Twisp models both accounts similarly: - An account set to represent the total balance of the account - A default account to represent debits/credits incurred by the account holder The difference between the two will be the [Balance Normality](/docs/accounting-core/chart-of-accounts#credit-normal-and-debit-normal) of the accounts involved, and the credit accounts will have an extra account to hold the credit line. #### Deposit Accounts The chart of accounts for end users will model a `Customer -> Account -> Card` hiearchical relationship. This hierarchical relationship we'll model via [Account Sets](/docs/reference/graphql/types/object#account-set). Each individual entity we'll create via a two objects as a building block for creating the chart of accounts: 1. An [Account Set](/docs/reference/graphql/types/object#account-set) to encapsulate the _total balance_ of the entity in question. 2. A default [Account](/docs/reference/graphql/types/object#account) as a member of the above account set for writing entries to the entity. ```mermaid graph BT SET[/Entity Account Set\] DEFAULT[/Entity Default Account\] DEFAULT --> SET ``` This building block can be encapsulated in via a GraphQL mutation: **Request** ```graphql mutation CreateEntityAccount( $accountSetId: UUID! $accountId: UUID! $code: String! $description: String $metadata: JSON $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $accountSetName: String! = "Entity Account Set" $accountName: String! = "Entity Default Account" ) { createAccountSet( input: { accountSetId: $accountSetId name: $accountSetName description: $description metadata: $metadata journalId: $journalId } ) { accountSetId } createAccount( input: { accountId: $accountId name: $accountName description: $description code: $code accountSetIds: [$accountSetId] metadata: $metadata } ) { accountId } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "37279a24-976e-4a16-805d-953245e5606f" }, "createAccount": { "accountId": "08ed9947-6775-47a7-81d3-b1cd84f4455a" } } } ``` **Variables** ```json { "accountSetId": "37279a24-976e-4a16-805d-953245e5606f", "accountId": "08ed9947-6775-47a7-81d3-b1cd84f4455a", "code": "37279a24-976e-4a16-805d-953245e5606f.DEFAULT", "description": "Account for Entity", "metadata": {} } ``` Once we have this building block, we can now model the chart of accounts creating and adding to the appropriate account set. Consider the use case of onboarding a customer, followed by creating a deposit account and issuing a debit card; After onboarding we'd expect to have the following end user chart of accounts: ```mermaid graph BT CUST[/Customer Account Set\] CUST_DEF[/Default Customer Account\] DEPOSIT[/Deposit Account Set\] DEPOSIT_DEF[/Default Deposit Account\] CARD[/Card Account Set\] CARD_DEF[/Default Card Account\] CUST_DEF & DEPOSIT --> CUST DEPOSIT_DEF & CARD --> DEPOSIT CARD_DEF --> CARD ``` The coordination of creating an account in Twisp can occur in many ways. For example, it may be in response to webhooks from a BaaS provider: | Webhook Received | Actions in Twisp | |--------------------|------------------------------------------------------------| | `customer.created` | Create customer account | | `account.created` | Create deposit account, add to customer account | | `card.created` | Create card account, add to corresponding deposit account. | Another might be creating Twisp entities via a state machine process, such as Temporal or Step Functions, coordinating activity with a number of vendors: **Request** ```graphql # OnboardCustomerDepositAccountCard does exactly that: # - Creates Account Set and Default Account for Customer # - Creates Account Set and Default Account for Deposit Account # - Adds Deposit Account to Customer # - Creates Account Set and Default Account for Card # - Adds Card to Deposit Account # This includes adding Unit metadata json. This can be # broken up into multiple mutations in response to a webhook # or used as-is in some kind of long running coordinated process # to onboard a customer. mutation OnboardCustomerDepositAccountCard( $customerAccountSetId: UUID! $customerAccountId: UUID! $customerAccountCode: String! $depositAccountSetId: UUID! $depositAccountId: UUID! $depositAccountCode: String! $cardAccountSetId: UUID! $cardAccountId: UUID! $cardAccountCode: String! $description: String $customerMetadata: JSON $depositMetadata: JSON $cardMetadata: JSON $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $customerAccountSetName: String! = "Customer Account Set" $customerAccountName: String! = "Customer Default Account" $depositAccountSetName: String! = "Deposit Account Set" $depositAccountName: String! = "Deposit Default Account" $cardAccountSetName: String! = "Card Account Set" $cardAccountName: String! = "Card Default Account" ) { # Onboard Customer customerSet: createAccountSet( input: { accountSetId: $customerAccountSetId name: $customerAccountSetName description: $description metadata: $customerMetadata journalId: $journalId } ) { accountSetId } customerDefault: createAccount( input: { accountId: $customerAccountId name: $customerAccountName description: $description code: $customerAccountCode accountSetIds: [$customerAccountSetId] metadata: $customerMetadata } ) { accountId } # Onboard Deposit Account depositSet: createAccountSet( input: { accountSetId: $depositAccountSetId name: $depositAccountSetName description: $description metadata: $depositMetadata journalId: $journalId } ) { accountSetId } depositDefault: createAccount( input: { accountId: $depositAccountId name: $depositAccountName description: $description code: $depositAccountCode metadata: $depositMetadata } ) { accountId } depositToCustomer: addToAccountSet( id: $customerAccountSetId member: { memberType: ACCOUNT_SET, memberId: $depositAccountSetId } ) { accountSetId } defaultDepositToSet: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT, memberId: $depositAccountId } ) { accountSetId } # Onboard Card cardSet: createAccountSet( input: { accountSetId: $cardAccountSetId name: $cardAccountSetName description: $description metadata: $cardMetadata journalId: $journalId } ) { accountSetId } cardDefault: createAccount( input: { accountId: $cardAccountId name: $cardAccountName description: $description code: $cardAccountCode metadata: $cardMetadata } ) { accountId } cardToDeposit: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT_SET, memberId: $cardAccountSetId } ) { accountSetId } defaultCardToSet: addToAccountSet( id: $cardAccountSetId member: { memberType: ACCOUNT, memberId: $cardAccountId } ) { accountSetId } } ``` **Response** ```json { "data": { "customerSet": { "accountSetId": "6669bc2c-84c1-4b76-9985-a33adeb00eae" }, "customerDefault": { "accountId": "ee5a7fa7-f336-4f70-b7e8-60473562e179" }, "depositSet": { "accountSetId": "80659068-3ac1-452a-b2da-2566e95283f8" }, "depositDefault": { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1" }, "depositToCustomer": { "accountSetId": "6669bc2c-84c1-4b76-9985-a33adeb00eae" }, "defaultDepositToSet": { "accountSetId": "80659068-3ac1-452a-b2da-2566e95283f8" }, "cardSet": { "accountSetId": "f141c872-7c66-4e6e-982c-fa1c45d94766" }, "cardDefault": { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9" }, "cardToDeposit": { "accountSetId": "80659068-3ac1-452a-b2da-2566e95283f8" }, "defaultCardToSet": { "accountSetId": "f141c872-7c66-4e6e-982c-fa1c45d94766" } } } ``` **Variables** ```json { "customerAccountSetId": "6669bc2c-84c1-4b76-9985-a33adeb00eae", "customerAccountId": "ee5a7fa7-f336-4f70-b7e8-60473562e179", "customerAccountCode": "6669bc2c-84c1-4b76-9985-a33adeb00eae.DEFAULT", "customerMetadata": { "type": "businessCustomer", "id": "1742784", "attributes": { "createdAt": "2024-03-06T18:53:48.431Z", "name": "Michael Parsons", "address": { "street": "Street name 1", "city": "City", "state": "CA", "postalCode": "11111", "country": "US" }, "phone": { "countryCode": "1", "number": "5555555555" }, "stateOfIncorporation": "CA", "ein": "123456789", "entityType": "Corporation", "contact": { "fullName": { "first": "Michael", "last": "Parsons" }, "email": "michael@twisp.com", "phone": { "countryCode": "1", "number": "5555555555" } }, "tags": {}, "authorizedUsers": [], "status": "Active" }, "relationships": { "org": { "data": { "type": "org", "id": "4975" } } } }, "depositAccountSetId": "80659068-3ac1-452a-b2da-2566e95283f8", "depositAccountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "depositAccountCode": "80659068-3ac1-452a-b2da-2566e95283f8.DEFAULT", "depositMetadata": { "id": "2890313", "type": "depositAccount", "attributes": { "hold": 0, "name": "Michael Parsons", "tags": { "purpose": "checking" }, "status": "Open", "balance": 0, "currency": "USD", "available": 0, "createdAt": "2024-03-13T20:47:56.985Z", "updatedAt": "2024-03-13T20:47:56.985Z", "routingNumber": "812345678", "depositProduct": "checking" }, "relationships": { "org": { "data": { "id": "4975", "type": "org" } }, "bank": { "data": { "id": "1", "type": "bank" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } }, "cardAccountSetId": "f141c872-7c66-4e6e-982c-fa1c45d94766", "cardAccountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "cardAccountCode": "f141c872-7c66-4e6e-982c-fa1c45d94766.DEFAULT", "cardMetadata": { "id": "1790431", "type": "businessDebitCard", "attributes": { "bin": "424242459", "tags": {}, "email": "richard@piedpiper.com", "phone": { "number": "5555555555", "countryCode": "1" }, "status": "Inactive", "address": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" }, "fullName": { "last": "Hendricks", "first": "Richard" }, "createdAt": "2024-03-13T20:56:00.442Z", "dateOfBirth": "2001-08-10", "last4Digits": "2860", "expirationDate": "2028-03", "shippingAddress": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" } }, "relationships": { "account": { "data": { "id": "2890313", "type": "account" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } }, "description": "Onboard customer, deposit account and card" } ``` This can be broken down into reusable pieces in case you want to add multiple deposit accounts or cards to a particular customer: **Request** ```graphql mutation CreateCustomer( $customerAccountSetId: UUID! $customerAccountId: UUID! $customerAccountCode: String! $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $customerAccountSetName: String! = "Customer Account Set" $customerAccountName: String! = "Customer Default Account" $customerMetadata: JSON $description: String = "Create a customer." ) { customerSet: createAccountSet( input: { accountSetId: $customerAccountSetId name: $customerAccountSetName description: $description metadata: $customerMetadata journalId: $journalId } ) { accountSetId } customerDefault: createAccount( input: { accountId: $customerAccountId name: $customerAccountName description: $description code: $customerAccountCode accountSetIds: [$customerAccountSetId] metadata: $customerMetadata } ) { accountId } } ``` **Response** ```json { "data": { "customerSet": { "accountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0" }, "customerDefault": { "accountId": "91378e87-7a08-403b-ae47-ad46f7385ede" } } } ``` **Variables** ```json { "customerAccountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0", "customerAccountId": "91378e87-7a08-403b-ae47-ad46f7385ede", "customerAccountCode": "83fff3de-3bea-4340-9d92-d099ada571a0.DEFAULT", "customerMetadata": { "type": "businessCustomer", "id": "1742784", "attributes": { "createdAt": "2024-03-06T18:53:48.431Z", "name": "Brian Parsons", "address": { "street": "Street name 1", "city": "City", "state": "CA", "postalCode": "11111", "country": "US" }, "phone": { "countryCode": "1", "number": "5555555555" }, "stateOfIncorporation": "CA", "ein": "123456789", "entityType": "Corporation", "contact": { "fullName": { "first": "Brian", "last": "Parsons" }, "email": "brian@twisp.com", "phone": { "countryCode": "1", "number": "5555555555" } }, "tags": {}, "authorizedUsers": [], "status": "Active" }, "relationships": { "org": { "data": { "type": "org", "id": "4975" } } } } } ``` **Request** ```graphql mutation CreateDepositAccount( $customerAccountSetId: UUID! $depositAccountSetId: UUID! $depositAccountId: UUID! $depositAccountCode: String! $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $depositAccountSetName: String! = "Deposit Account Set" $depositAccountName: String! = "Deposit Default Account" $depositMetadata: JSON $description: String = "create a deposit account." ) { depositSet: createAccountSet( input: { accountSetId: $depositAccountSetId name: $depositAccountSetName description: $description metadata: $depositMetadata journalId: $journalId } ) { accountSetId } depositDefault: createAccount( input: { accountId: $depositAccountId name: $depositAccountName description: $description code: $depositAccountCode metadata: $depositMetadata } ) { accountId } depositToCustomer: addToAccountSet( id: $customerAccountSetId member: { memberType: ACCOUNT_SET, memberId: $depositAccountSetId } ) { accountSetId } defaultDepositToSet: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT, memberId: $depositAccountId } ) { accountSetId } } ``` **Response** ```json { "data": { "depositSet": { "accountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e" }, "depositDefault": { "accountId": "9963d6d2-2458-4f67-8bed-7604a5e0281e" }, "depositToCustomer": { "accountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0" }, "defaultDepositToSet": { "accountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e" } } } ``` **Variables** ```json { "customerAccountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0", "depositAccountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e", "depositAccountId": "9963d6d2-2458-4f67-8bed-7604a5e0281e", "depositAccountCode": "8801ced3-e6a3-4af6-b01a-514473051d4e.DEFAULT", "depositMetadata": { "id": "2890313", "type": "depositAccount", "attributes": { "hold": 0, "name": "Brian Parsons", "tags": { "purpose": "checking" }, "status": "Open", "balance": 0, "currency": "USD", "available": 0, "createdAt": "2024-03-13T20:47:56.985Z", "updatedAt": "2024-03-13T20:47:56.985Z", "routingNumber": "812345678", "depositProduct": "checking" }, "relationships": { "org": { "data": { "id": "4975", "type": "org" } }, "bank": { "data": { "id": "1", "type": "bank" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } } } ``` **Request** ```graphql mutation CreateCard( $depositAccountSetId: UUID! $cardAccountSetId: UUID! $cardAccountId: UUID! $cardAccountCode: String! $cardAccountSetName: String! = "Card Account Set" $cardAccountName: String! = "Card Default Account" $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $cardMetadata: JSON $description: String = "Create a card." ) { cardSet: createAccountSet( input: { accountSetId: $cardAccountSetId name: $cardAccountSetName description: $description metadata: $cardMetadata journalId: $journalId } ) { accountSetId } cardDefault: createAccount( input: { accountId: $cardAccountId name: $cardAccountName description: $description code: $cardAccountCode metadata: $cardMetadata } ) { accountId } cardToDeposit: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT_SET, memberId: $cardAccountSetId } ) { accountSetId } defaultCardToSet: addToAccountSet( id: $cardAccountSetId member: { memberType: ACCOUNT, memberId: $cardAccountId } ) { accountSetId } } ``` **Response** ```json { "data": { "cardSet": { "accountSetId": "151d8647-09af-4d5a-ac32-39fcd55d2454" }, "cardDefault": { "accountId": "f6318c3e-4996-4d88-ba7a-0f66cb04302b" }, "cardToDeposit": { "accountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e" }, "defaultCardToSet": { "accountSetId": "151d8647-09af-4d5a-ac32-39fcd55d2454" } } } ``` **Variables** ```json { "depositAccountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e", "cardAccountSetId": "151d8647-09af-4d5a-ac32-39fcd55d2454", "cardAccountId": "f6318c3e-4996-4d88-ba7a-0f66cb04302b", "cardAccountCode": "151d8647-09af-4d5a-ac32-39fcd55d2454.DEFAULT", "cardMetadata": { "id": "1790431", "type": "businessDebitCard", "attributes": { "bin": "424242459", "tags": {}, "email": "richard@piedpiper.com", "phone": { "number": "5555555555", "countryCode": "1" }, "status": "Inactive", "address": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" }, "fullName": { "last": "Hendricks", "first": "Richard" }, "createdAt": "2024-03-13T20:56:00.442Z", "dateOfBirth": "2001-08-10", "last4Digits": "2860", "expirationDate": "2028-03", "shippingAddress": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" } }, "relationships": { "account": { "data": { "id": "2890313", "type": "account" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } } } ``` Regardless of the mechanics of how/when new accounts are added, we suggest association of your own internal and third party identifiers on Twisp entities so that you can easily look up items in the future. > **Tip:** > > Picking identifiers for Twisp and ensuring there is enough data in Twisp to look up by Unit identifiers, or your own system internal identifiers is paramount for keeping data consistent. Twisp provides a few different mechanisms to aid in this: > > - Accounts have an `externalId` field that will enforce uniqueness. > - Accounts and Account Sets have a `metadata` field that's useful for populating with metadata from external systems, especially identifiers > - Utilize [UUID v5](https://www.sohamkamani.com/uuid-versions-explained/#v5-non-random-uuids) to generate deterministic identifiers for Twisp entities that correspond to their Unit counterparts. #### Credit Accounts Credit accounts are modeled slightly differently than deposit accounts: ```mermaid graph BT SET[/Credit Balance Account Set\] PAYABLE[/Credit Payable Account Set\] DEFAULT[/Credit Default Account\] LINE[/Credit Line\] DEFAULT --> PAYABLE LINE & PAYABLE --> SET ``` In this case a second account, `Credit Line` is added. This account is where we book a single entry to define the limit of the entire credit account. The other accounts function identically with the normality of the `Credit Payable` accounts being **Debit Normal**: | Entry ID | Account | Amount | Direction | Credit Balance | Payable Balance | |----------|---------|--------|-----------|----------------|-----------------| | 1 | Line | $1000 | Credit | $1000 | $0 | | 2 | Default | $500 | Debit | $500 | $500 | | 3 | Default | $250 | Credit | $750 | $250 | | 4 | Default | $10 | Debit | $740 | $260 | This gives you two significant balances: 1. The `Credit Balance` set gives you a balance that you can use for authorization decisions. 2. The `Credit Payable` set gives you a balance that is owed to you by the customer. **Request** ```graphql mutation CreateCreditAccount( $balanceSetId: UUID! $payableSetId: UUID! $defaultAccountId: UUID! $lineAccountId: UUID! $defaultCode: String! $lineCode: String! $metadata: JSON $journalId: UUID! = "00000000-0000-0000-0000-000000000000" ) { balanceSet: createAccountSet( input: { accountSetId: $balanceSetId name: "Credit Balance" description: "Credit balance roll up for this credit account." metadata: $metadata journalId: $journalId } ) { accountSetId } payableSet: createAccountSet( input: { accountSetId: $payableSetId name: "Payable Balance" description: "Outstanding balance owed by account holder." metadata: $metadata journalId: $journalId } ) { accountSetId } lineAcct: createAccount( input: { accountId: $lineAccountId name: "Credit Line" description: "Apply credit limit to this account." code: $lineCode metadata: $metadata } ) { accountId } defaultAccount: createAccount( input: { accountId: $defaultAccountId name: "Default" description: "Default credit account" code: $defaultCode metadata: $metadata } ) { accountId } } ``` **Response** ```json { "data": { "balanceSet": { "accountSetId": "3b270302-a595-4942-a166-d5e2ab8d6a19" }, "payableSet": { "accountSetId": "3d0d7d8c-19c7-4835-98ea-979077e0972c" }, "lineAcct": { "accountId": "b7ad3970-c408-4a01-83af-8f1523b407b5" }, "defaultAccount": { "accountId": "154396fb-5114-4a9b-afb2-9276dab92570" } } } ``` **Variables** ```json { "balanceSetId": "3b270302-a595-4942-a166-d5e2ab8d6a19", "payableSetId": "3d0d7d8c-19c7-4835-98ea-979077e0972c", "defaultAccountId": "154396fb-5114-4a9b-afb2-9276dab92570", "lineAccountId": "b7ad3970-c408-4a01-83af-8f1523b407b5", "defaultCode": "CREDIT.3b270302-a595-4942-a166-d5e2ab8d6a19.DEFAULT", "lineCode": "LINE.3b270302-a595-4942-a166-d5e2ab8d6a19.DEFAULT", "metadata": {} } ``` ### Platform Accounts When operating a fintech, your organization will partner with a bank which will configure one or more actual bank accounts in order to support your operations. These include, but are not limited to: | Account | Description | |-------------------------|-----------------------------------------------------------------------------------------| | Suspense Account | An account to post transactions with unknown accounts to. | | ACH Settlement | Settlement account for ACHs. | | Bill Pay Settlement | Settlement account for Bill Pay. | | Foreign Checks Account | Settlement account for foriegn checks. | | Courtesy Credit Account | Operational accounts for crediting accounts via customer service interactions. | | Charge Off Account | Operational account to write off closing balances on uncollectable accounts. | | Fraud Losses | Operational account to write off losses for fraud. | | Levies & Garnishments | Account to collect levies and garnishments of funds. | | Cashiers Check | Settlement account for cashiers checks. | | Card Disputes | Reserve account for handling card related disputes. | | ACH Disputes | Reserve account for handling ACH related disputes. | | Interchange Revenue | Revenue account for shared interchange. | | Collected Fee Revenue | Revenue account for fees collected from accounts. | | FBO | "For Benefit Of", omnibus accounts for specific purpose. For example, virtual wallets. | | Cash | An asset account that represents all funds created in the system. | These accounts are often the "other side" of your double entry accounting and you could have multiple of these accounts across your various banking partners. For the purpose of this document, we'll consider a system with: - Cash account for assets - Card Settlement for a single BIN - ACH Settlement account for single bank partner - Suspense Account - Disputes - Revenue > **Tip:** > > There will be a few well known identifiers for these settlement accounts that will be pervasively used through the system. > > We recommend creating a library with human readable codes that allow fast look ups to use as parameters to tran codes. **Request** ```graphql mutation PlatformAccounts { cash: createAccount( input: { accountId: "db07e5cf-6cd7-4629-a952-9613578cd8ea" code: "ASSETS.CASH" name: "Cash account" description: "Cash account representing all assets the platform." normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } cardSettlement: createAccount( input: { accountId: "fabdce1f-6eb9-4630-9472-6338ed3dc6a2" code: "SETTLEMENT.VISA" name: "Visa Settlement Account" description: "Settlement account to use for Visa cards on bin 41200." config: { enableConcurrentPosting: true } } ) { accountId } achSettlement: createAccount( input: { accountId: "7c923aed-a5fc-4ded-b2f0-57db45a8b545" code: "SETTLEMENT.ACH" name: "ACH Settlement Account" description: "Settlement account to use for Bank Partner 1." config: { enableConcurrentPosting: true } } ) { accountId } suspenseAccount: createAccount( input: { accountId: "6e341e23-e35f-488c-b947-b7b8839f4c00" code: "SUSPENSE" name: "Suspense Account" description: "Account to post to when an account doesnt exist or is in a non-postable state." config: { enableConcurrentPosting: true } } ) { accountId } disputes: createAccount( input: { accountId: "08f304d5-6a63-40b9-a182-bfb732994b15" code: "DISPUTES" name: "Disputes Account" description: "Reserve account for dispute resolution." config: { enableConcurrentPosting: true } } ) { accountId } revenueAccount: createAccount( input: { accountId: "4d79af07-acd9-4a44-8291-78a573ced41d" code: "REVENUE" name: "Revenue Account" description: "Interchange and other fees collected from customer accounts." config: { enableConcurrentPosting: true } } ) { accountId } } ``` **Response** ```json { "data": { "cash": { "accountId": "db07e5cf-6cd7-4629-a952-9613578cd8ea" }, "cardSettlement": { "accountId": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2" }, "achSettlement": { "accountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545" }, "suspenseAccount": { "accountId": "6e341e23-e35f-488c-b947-b7b8839f4c00" }, "disputes": { "accountId": "08f304d5-6a63-40b9-a182-bfb732994b15" }, "revenueAccount": { "accountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } } } ``` ## Transaction Workflows When processing transactions, to end users they feel like a singular event. I swipe my card the purchase is made. However that singular event is often a series of transactions, depending on how the swipe was made (Dual vs Single message auth), or if the merchant uses multiple clearings. Card processing can be challenging as the underlying protocol allows for a wide variety of behaviors. The same is true with many other types of transactions, such as ACH's or checks. The logically singular event may have a lifecycle that encompasses many transactions. These lifecycles we call transaction workflows and here we present a simplified model that you can adapt to your particular card and ach providers. ### Primitives There are a few different primitives built into Twisp for posting transactions that will help us model these the workflows: - [Tran Codes](/docs/accounting-core/encoded-transactions) define entries are posted for a particular transaction type. - [postTransaction](/docs/reference/graphql/mutations#post-transaction) allows you to post a transaction with a specific tran code. - [voidTransaction](/docs/reference/graphql/mutations#void-transaction) posts the entries required to reverse any transaction. - [workflows](/docs/reference/graphql/mutations#workflows) allow composition of multiple transaction operations to fulfill specific use cases. Each of these transaction flows will be composed together of one or more of the above primitives. These workflows are not prescriptive, but are intended to illustrate the concepts to the point where they can be adapted to your own use case and production usage. ## Card Authorizations and Transactions The ISO-8583 specification defines how merchants and issuers interact with each other via card networks. This communication protocol is implemented by a wide variety of vendors, and for the purposes of this document we're going to explore the key workflows required, define the [tran codes](/docs/accounting-core/encoded-transactions) required to post transaction and illustrate how to utilize those tran codes to fulfill card authorization work flows. ### Tran Codes At the heart of processing transactions in Twisp are the Tran Codes required to make journal entries. Here is a set of tran codes that allow you to completely model card authorization and settlement/clearing. | Code | Description | |-------------------|-------------------------------------------------------------| | CARD_HOLD | Post at pending layer between settlement and card accounts. | | CARD_SETTLE | Post at settled layer between settlement and card accounts. | | CARD_HOLD_VOID | Optional $0 entries at pending layer | | CARD_HOLD_REPLACE | Identical to CARD_HOLD, but provides labeling differences | | CARD_DECLINE | Optional $0 entries at pending layer for declines | **Request** ```graphql mutation CardTranCodes( $cardHold: TranCodeInput! $cardHoldVoid: TranCodeInput! $cardHoldReplace: TranCodeInput! $cardSettle: TranCodeInput! $cardDecline: TranCodeInput! ) { cardHold: createTranCode(input: $cardHold) { ...TC } cardHoldVoid: createTranCode(input: $cardHoldVoid) { ...TC } cardHoldReplace: createTranCode(input: $cardHoldReplace) { ...TC } cardSettle: createTranCode(input: $cardSettle) { ...TC } cardDecline: createTranCode(input: $cardDecline) { ...TC } } fragment TC on TranCode { tranCodeId code description params { name type default description } transaction { effective journalId correlationId externalId description metadata } entries { entryType accountId layer direction units currency description metadata condition } } ``` **Response** ```json { "data": { "cardHold": { "tranCodeId": "9f4f3293-539d-4c81-b3b9-3c8d94fa67ba", "code": "CARD_HOLD", "description": "Place an authorization hold on an account for the amount.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldVoid": { "tranCodeId": "218f19fc-e283-4f5b-90f6-02fd99e48e5c", "code": "CARD_HOLD_VOID", "description": "Posts $0 entries to an account indicating a card hold was voided. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "'Correlation identifier to group related transactions.'" }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_VOID_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_VOID_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldReplace": { "tranCodeId": "b0b6cb20-7c9d-4e48-8179-caa9c06e44d7", "code": "CARD_HOLD_REPLACE", "description": "Posts a hold at pending layer indicating the hold amount changed to this value. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_REPLACE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_REPLACE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardSettle": { "tranCodeId": "2e1f3ed4-0f0e-467a-82f7-715c2bcd97ef", "code": "CARD_SETTLE", "description": "Post a card settlement at the settled layer.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_SETTLE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_SETTLE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardDecline": { "tranCodeId": "2b954d71-01a8-4021-82a1-c78aa1b607bd", "code": "CARD_DECLINE", "description": "Note a card decline by posting a $0 transaction. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_DECLINE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_DECLINE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] } } } ``` **Variables** ```json { "cardHold": { "tranCodeId": "9f4f3293-539d-4c81-b3b9-3c8d94fa67ba", "code": "CARD_HOLD", "description": "Place an authorization hold on an account for the amount.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldVoid": { "tranCodeId": "218f19fc-e283-4f5b-90f6-02fd99e48e5c", "code": "CARD_HOLD_VOID", "description": "Posts $0 entries to an account indicating a card hold was voided. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "'Correlation identifier to group related transactions.'" }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_VOID_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_VOID_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldReplace": { "tranCodeId": "b0b6cb20-7c9d-4e48-8179-caa9c06e44d7", "code": "CARD_HOLD_REPLACE", "description": "Posts a hold at pending layer indicating the hold amount changed to this value. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_REPLACE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_REPLACE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardSettle": { "tranCodeId": "2e1f3ed4-0f0e-467a-82f7-715c2bcd97ef", "code": "CARD_SETTLE", "description": "Post a card settlement at the settled layer.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_SETTLE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_SETTLE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardDecline": { "tranCodeId": "2b954d71-01a8-4021-82a1-c78aa1b607bd", "code": "CARD_DECLINE", "description": "Note a card decline by posting a $0 transaction. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_DECLINE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_DECLINE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] } } ``` ### Card Authorization Workflows Card flows in Twisp can be modeled entirely with `postTransaction` and `voidTransaction` which are the most common primitives you'll use in Twisp. Together with the set of card tran codes, you can build out very simple card flows that allow you to accurately track the balances and entries required for a card transaction lifecycle. Using the account we onboarded earlier, we'll post the transactions for each use case and print the resulting balances. #### Authorization Approval You receive an authorization request and/or an advice for $10 that an authorization was approved. | Event | Operation | Tran Code | Description | |--------------|-----------------|-----------|--------------------------------| | auth.request | postTransaction | CARD_HOLD | post $ amount to pending layer | | auth.created | voidTransaction | n/a | void previous transaction | | ... | postTransaction | CARD_HOLD | post $ amount to pending layer | **Request** ```graphql mutation CardAuthorizationApproval( $initialAuthorizationId: UUID! $authorizationId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "81832b7a-8881-48fe-b309-3bff1fcfc986" }, "initialAuthorization": { "transactionId": "2f05a4d4-07c5-42c1-a567-438737b7a0ab" }, "voidInitialAuthorization": { "transactionId": "988c1dce-ad47-52e3-8d37-4d79b60c8e66" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "initialAuthorizationId": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "authorizationId": "81832b7a-8881-48fe-b309-3bff1fcfc986", "correlation": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "effective": "2022-10-01", "initAuthHook": "{\"status\":\"AUTHORIZATION\",\"amount\":1000,\"acquirer_fee\":100,\"authorization_amount\":1000,\"settled_amount\":0,\"events\":[],\"created\":\"2022-10-01T00:00:00Z\",\"token\":\"2f05a4d4-07c5-42c1-a567-438737b7a0ab\"}", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"81832b7a-8881-48fe-b309-3bff1fcfc986\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"ef867c4d-53fc-49e5-ba8c-342209eb9b5c\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$10.00" } } } } } ``` #### Authorization Decline You decide to decline an authorization request or receive an advice that an authorization was declined for $10. | Event | Operation | Tran Code | Description | |--------------|-----------------|----------------|---------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | ... | check balance | n/a | balance exceeds threshold | | ... | voidTransaction | n/a | void transaction | | ... | postTransaction | CARD_HOLD_VOID | optionally Post $0 entry indicating no hold | **Request** ```graphql mutation CardAuthorizationDecline( $initialAuthorizationId: UUID! $declineId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $declineHook: JSON ) { # Received initial authorization webhook # and return resulting balances. initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId entries(first: 4) { nodes { balance { available(layer: PENDING) { normalBalance { units } } } } } } # Declined transaction, later recieved declined webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } decline: postTransaction( input: { transactionId: $declineId tranCode: "CARD_DECLINE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $declineHook } } ) { transactionId } } ``` **Response** ```json { "data": { "decline": { "transactionId": "3318a65f-2245-4058-bd6c-9bee76f8b0af" }, "initialAuthorization": { "entries": { "nodes": [ { "balance": { "available": { "normalBalance": { "units": "-20.00" } } } }, { "balance": { "available": { "normalBalance": { "units": "10.00" } } } } ] }, "transactionId": "67734487-03fc-4451-b9f3-c89fce63ab8f" }, "voidInitialAuthorization": { "transactionId": "de376aa4-0163-5294-a9d2-d44389fb24b5" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "initialAuthorizationId": "67734487-03fc-4451-b9f3-c89fce63ab8f", "declineId": "3318a65f-2245-4058-bd6c-9bee76f8b0af", "correlation": "67734487-03fc-4451-b9f3-c89fce63ab8f", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"67734487-03fc-4451-b9f3-c89fce63ab8f\"\n }", "declineHook": "{\n \"status\": \"DECLINED\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"UNAUTHORIZED_MERCHANT\",\n \"token\": \"3318a65f-2245-4058-bd6c-9bee76f8b0af\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"67734487-03fc-4451-b9f3-c89fce63ab8f\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$10.00" } } } } } ``` #### Authorization Update An authorization is approved for $10 and then updated to $1. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------------|------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | auth.update | voidTransaction | n/a | void prior transaction | | ... | postTransaction | CARD_HOLD_REPLACE | post new $ amount to pending layer | **Request** ```graphql mutation CardAuthorizationUpdate( $initialAuthorizationId: UUID! $authorizationId: UUID! $updateId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $updateAmount: Decimal $effective: Date $initAuthHook: JSON $authHook: JSON $updateHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Received update webhook voidApprovedAuth: voidTransaction(id: $authorizationId) { transactionId } update: postTransaction( input: { transactionId: $updateId tranCode: "CARD_HOLD_REPLACE" params: { account: $accountId amount: $updateAmount correlation: $correlation effective: $effective metadata: $updateHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "79c336e6-8782-4df5-9992-c7c9bab0ad4e" }, "initialAuthorization": { "transactionId": "449afa1d-95dd-406f-9d72-c8b4396055f9" }, "update": { "transactionId": "c97c23d5-02c1-4361-b371-f1e40c50a73f" }, "voidApprovedAuth": { "transactionId": "48494e7e-553d-540e-a1aa-a2f3533c29b3" }, "voidInitialAuthorization": { "transactionId": "7d672168-0883-5f58-9fb7-6f1f89e42e48" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "updateAmount": "1.00", "updateId": "c97c23d5-02c1-4361-b371-f1e40c50a73f", "initialAuthorizationId": "449afa1d-95dd-406f-9d72-c8b4396055f9", "authorizationId": "79c336e6-8782-4df5-9992-c7c9bab0ad4e", "correlation": "449afa1d-95dd-406f-9d72-c8b4396055f9", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"449afa1d-95dd-406f-9d72-c8b4396055f9\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"79c336e6-8782-4df5-9992-c7c9bab0ad4e\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"449afa1d-95dd-406f-9d72-c8b4396055f9\"\n }", "updateHook": "{\n \"status\": \"PENDING\",\n \"amount\": 100,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 100,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"79c336e6-8782-4df5-9992-c7c9bab0ad4e\"\n },\n {\n \"amount\": -900,\n \"type\": \"VOID\",\n \"result\": \"APPROVED\",\n \"token\": \"c97c23d5-02c1-4361-b371-f1e40c50a73f\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"449afa1d-95dd-406f-9d72-c8b4396055f9\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$11.00" } } } } } ``` #### Authorization Cancelation The authorization is created for $10 that expires or is canceled by the merchant. | Event | Operation | Tran Code | Description | |--------------|-----------------|--------------|-----------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | auth.cancel | voidTransaction | n/a | void prior transaction | **Request** ```graphql mutation CardAuthorizationCancel( $initialAuthorizationId: UUID! $authorizationId: UUID! $expireId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON $expireHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Recieved card expiry webhook voidAuthorization: voidTransaction(id: $authorizationId) { transactionId } expire: postTransaction( input: { transactionId: $expireId tranCode: "CARD_HOLD_VOID" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $expireHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "b73a6058-d423-4543-9aba-0ff54c4e0c27" }, "expire": { "transactionId": "db083442-d2a3-45a9-abe5-24023fdb10fb" }, "initialAuthorization": { "transactionId": "a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c" }, "voidAuthorization": { "transactionId": "9fe6671e-aba5-5b4c-a986-739271521c8a" }, "voidInitialAuthorization": { "transactionId": "14984ed2-b2b6-551f-b9c8-32b32c1f2590" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "initialAuthorizationId": "a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c", "authorizationId": "b73a6058-d423-4543-9aba-0ff54c4e0c27", "expireId": "db083442-d2a3-45a9-abe5-24023fdb10fb", "correlation": "a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"b73a6058-d423-4543-9aba-0ff54c4e0c27\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c\"\n }", "expireHook": "{\n \"status\": \"VOIDED\",\n \"amount\": 0,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 0,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"b73a6058-d423-4543-9aba-0ff54c4e0c27\"\n },\n {\n \"amount\": -1000,\n \"type\": \"AUTHORIZATION_EXPIRY\",\n \"result\": \"APPROVED\",\n \"token\": \"db083442-d2a3-45a9-abe5-24023fdb10fb\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$11.00" } } } } } ``` #### Settlement (amount >= auth) A settlement of an existing authorization whose amount is greater than the hold amount. In this case a $10 settlement is applied to the account. | Event | Operation | Tran Code | Description | |--------------|-----------------|----------------|---------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | | ... | voidTransaction | n/a | void prior authorization transaction | | ... | postTransaction | CARD_HOLD_VOID | optionally Post $0 entry indicating no hold | **Request** ```graphql mutation CardSettlement( $initialAuthorizationId: UUID! $authorizationId: UUID! $settleId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON $settleHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Recieved clearing webhook voidAuthorization: voidTransaction(id: $authorizationId) { transactionId } clearing: postTransaction( input: { transactionId: $settleId tranCode: "CARD_SETTLE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "5e20e176-82bd-49b4-955c-c059b153b5db" }, "clearing": { "transactionId": "108449c6-122c-43a4-936c-228056e8ee58" }, "initialAuthorization": { "transactionId": "3b8937a2-bf03-4bd4-83e8-ededf56118e9" }, "voidAuthorization": { "transactionId": "19c18c18-5a6d-505c-846b-08debddde872" }, "voidInitialAuthorization": { "transactionId": "6a27101a-b1c4-5f54-a510-59fcf5667fe2" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "settleId": "108449c6-122c-43a4-936c-228056e8ee58", "initialAuthorizationId": "3b8937a2-bf03-4bd4-83e8-ededf56118e9", "authorizationId": "5e20e176-82bd-49b4-955c-c059b153b5db", "correlation": "3b8937a2-bf03-4bd4-83e8-ededf56118e9", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3b8937a2-bf03-4bd4-83e8-ededf56118e9\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"5e20e176-82bd-49b4-955c-c059b153b5db\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3b8937a2-bf03-4bd4-83e8-ededf56118e9\"\n }", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": 1000,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 0,\n \"settled_amount\": 1000,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"5e20e176-82bd-49b4-955c-c059b153b5db\"\n },\n {\n \"amount\": 1000,\n \"type\": \"CLEARING\",\n \"result\": \"APPROVED\",\n \"token\": \"108449c6-122c-43a4-936c-228056e8ee58\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3b8937a2-bf03-4bd4-83e8-ededf56118e9\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$10.00" } }, "pending": { "normalBalance": { "formatted": "-$21.00" } } } } } ``` #### Multi-settlement (amount < auth) Some processors always drop holds with a settlement. Others will adjust hold amount and allow for additional settlements. You'll map your implementation to match the card providers. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------------|---------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | | ... | voidTransaction | n/a | void prior authorization transaction | | ... | postTransaction | CARD_HOLD_VOID | optionally Post $0 entry indicating no hold | | auth.updated | voidTransaction | n/a | optionally void prior $0 hold | | ... | postTransaction | CARD_HOLD_REPLACE | optionally post new hold | **Request** ```graphql mutation CardPartialSettlement( $initialAuthorizationId: UUID! $authorizationId: UUID! $partialSettleId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $replaceAmount: Decimal! $settleAmount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON $settleHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Recieved partial settlement webhook voidAuthorization: voidTransaction(id: $authorizationId) { transactionId } updateHold: postTransaction( input: { transactionId: $partialSettleId tranCode: "CARD_HOLD_REPLACE" params: { account: $accountId amount: $replaceAmount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } partialSettle: postTransaction( input: { transactionId: "6daad172-6ae7-4ddd-843c-e5e0455fb6da" tranCode: "CARD_SETTLE" params: { account: $accountId amount: $settleAmount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "9588d5a5-26a2-4ed0-84bb-384f4097c7f5" }, "initialAuthorization": { "transactionId": "3175506b-d828-4deb-ae6b-5aabeebd438d" }, "partialSettle": { "transactionId": "6daad172-6ae7-4ddd-843c-e5e0455fb6da" }, "updateHold": { "transactionId": "3c9faea9-1807-4867-967a-0fadd0b18521" }, "voidAuthorization": { "transactionId": "bcbf86b3-101c-5605-b872-6587d381c477" }, "voidInitialAuthorization": { "transactionId": "4f87f582-d440-558b-801d-4176f0056cc8" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "replaceAmount": "9.00", "settleAmount": "1.00", "initialAuthorizationId": "3175506b-d828-4deb-ae6b-5aabeebd438d", "authorizationId": "9588d5a5-26a2-4ed0-84bb-384f4097c7f5", "partialSettleId": "3c9faea9-1807-4867-967a-0fadd0b18521", "correlation": "3175506b-d828-4deb-ae6b-5aabeebd438d", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3175506b-d828-4deb-ae6b-5aabeebd438d\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"9588d5a5-26a2-4ed0-84bb-384f4097c7f5\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3175506b-d828-4deb-ae6b-5aabeebd438d\"\n }", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": 100,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 900,\n \"settled_amount\": 100,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"9588d5a5-26a2-4ed0-84bb-384f4097c7f5\"\n },\n {\n \"amount\": 100,\n \"type\": \"CLEARING\",\n \"result\": \"APPROVED\",\n \"token\": \"3c9faea9-1807-4867-967a-0fadd0b18521\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3175506b-d828-4deb-ae6b-5aabeebd438d\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$11.00" } }, "pending": { "normalBalance": { "formatted": "-$31.00" } } } } } ``` #### Settlement (no Auth) Receive a transaction without a matching hold. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------|---------------------------------------------| | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | **Request** ```graphql mutation CardForcePost( $settlementId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $settleHook: JSON ) { # Received clearing webhook. forcePost: postTransaction( input: { transactionId: $settlementId tranCode: "CARD_SETTLE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } } ``` **Response** ```json { "data": { "forcePost": { "transactionId": "d0150140-0ef1-4b89-8c23-7ad537006329" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "settlementId": "d0150140-0ef1-4b89-8c23-7ad537006329", "correlation": "3a47f3fc-de3b-44b9-aea2-74e0c02e1fb8", "effective": "2022-10-01", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": 1000,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 1000,\n \"settled_amount\": 1000,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"CLEARING\",\n \"result\": \"APPROVED\",\n \"token\": \"d0150140-0ef1-4b89-8c23-7ad537006329\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3a47f3fc-de3b-44b9-aea2-74e0c02e1fb8\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$21.00" } }, "pending": { "normalBalance": { "formatted": "-$41.00" } } } } } ``` #### Return Receive a return transaction. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------|---------------------------------------------| | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | **Request** ```graphql mutation CardReturn( $settlementId: UUID! $accountId: UUID! $correlation: String $direction: String $amount: Decimal! $effective: Date $settleHook: JSON ) { # Received clearing webhook. forcePost: postTransaction( input: { transactionId: $settlementId tranCode: "CARD_SETTLE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $settleHook direction: $direction } } ) { transactionId } } ``` **Response** ```json { "data": { "forcePost": { "transactionId": "fa5cb793-0739-4eb8-8db2-9b452ed71930" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "direction": "CREDIT", "settlementId": "fa5cb793-0739-4eb8-8db2-9b452ed71930", "correlation": "265a9120-e42a-4c02-b3dc-eb80a2e3cf00", "effective": "2022-10-01", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": -1000,\n \"acquirer_fee\": 0,\n \"authorization_amount\": -1000,\n \"settled_amount\": -1000,\n \"events\": [\n {\n \"amount\": -1000,\n \"type\": \"RETURN\",\n \"result\": \"APPROVED\",\n \"token\": \"fa5cb793-0739-4eb8-8db2-9b452ed71930\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"265a9120-e42a-4c02-b3dc-eb80a2e3cf00\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$11.00" } }, "pending": { "normalBalance": { "formatted": "-$31.00" } } } } } ``` ## ACH The Automated Clearing House is a service provided by the Federal Reserve for conducting electronic funds transfers. There are two modes of operation when dealing with ACH: - Receiving Deposit Financial Institution (RDFI): External FI's are crediting/debiting your accounts with theirs - Originating Deposit Financial Institution (ODFI): You are initiating credits/debits with external accounts Twisp has a number of built in tran codes and workflows to handle both use cases. ### Tran Codes **Request** ```graphql query ACHTranCodes { achEncumbranceCancelDebit: tranCode( id: "7b24410d-6dbc-44cd-8779-a1bfa827868a" ) { ...TC } achEncumbranceCancelReversalCredit: tranCode( id: "b7976c0e-2e4c-4e99-a48f-86384f3dddf6" ) { ...TC } achEncumbranceCredit: tranCode(id: "8fe41dca-190a-4c84-adc9-78952ac70a54") { ...TC } achEncumbranceDebit: tranCode(id: "0b83d0d4-b580-4056-a094-74ef393328b7") { ...TC } achEncumbranceReturnDebit: tranCode( id: "cde02e54-e725-46bc-9b61-5511c93058a7" ) { ...TC } achEncumbranceReturnCredit: tranCode( id: "4c4d9599-1caa-4632-a6fb-ca1ae3a2b836" ) { ...TC } achEncumbranceReversalDebit: tranCode( id: "cd7b997c-526c-4b8f-ac56-8c6de009dc25" ) { ...TC } achEncumbranceReversalCredit: tranCode( id: "f57ba3a2-ccfa-47de-ae81-1a8167639fe3" ) { ...TC } achFeeDebit: tranCode(id: "d3b64ee6-0815-4d06-aa9b-906bc48cbc19") { ...TC } achFeeReimburseCredit: tranCode(id: "49b0f890-75e7-4b94-920b-54328c02b2ca") { ...TC } achPendingDebit: tranCode(id: "e19dfe3f-6513-425a-b9a3-870f91303cc8") { ...TC } achPendingCancelCredit: tranCode(id: "c66725d7-c115-4339-89e9-69b6a6ae97bd") { ...TC } achPendingCancelReversalDebit: tranCode( id: "f83a1768-06a1-4a6b-a472-6e9b9c7dbe75" ) { ...TC } achPendingReversalDebit: tranCode( id: "21036f96-bc05-4526-8004-e63ff6955d0a" ) { ...TC } achSettleCredit: tranCode(id: "d918eb34-1ef9-4437-a82b-46c169f63e41") { ...TC } achSettleDebit: tranCode(id: "5af51352-84ad-4534-aacf-4e68c2ed2e2b") { ...TC } achSettleReturnCredit: tranCode(id: "90e09ed8-ae9c-46f2-9be8-2e09b4a5de35") { ...TC } achSettleReturnDebit: tranCode(id: "2b11b428-54ed-429c-97d9-b9a4e3ef6007") { ...TC } } fragment TC on TranCode { tranCodeId code description params { name type default description } transaction { effective journalId correlationId externalId description metadata } entries { entryType accountId layer direction units currency description metadata condition } } ``` **Response** ```json { "data": { "achEncumbranceCancelDebit": { "tranCodeId": "7b24410d-6dbc-44cd-8779-a1bfa827868a", "code": "SYS_ACH_ENCUMBRANCE_CANCEL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CANCEL_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_CANCEL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceCancelReversalCredit": { "tranCodeId": "b7976c0e-2e4c-4e99-a48f-86384f3dddf6", "code": "SYS_ACH_ENCUMBRANCE_CANCEL_REVERSAL_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CANCEL_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_CANCEL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceCredit": { "tranCodeId": "8fe41dca-190a-4c84-adc9-78952ac70a54", "code": "SYS_ACH_ENCUMBRANCE_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceDebit": { "tranCodeId": "0b83d0d4-b580-4056-a094-74ef393328b7", "code": "SYS_ACH_ENCUMBRANCE_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReturnDebit": { "tranCodeId": "cde02e54-e725-46bc-9b61-5511c93058a7", "code": "SYS_ACH_ENCUMBRANCE_RETURN_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_RETURN_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_RETURN_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReturnCredit": { "tranCodeId": "4c4d9599-1caa-4632-a6fb-ca1ae3a2b836", "code": "SYS_ACH_ENCUMBRANCE_RETURN_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_RETURN_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_RETURN_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReversalDebit": { "tranCodeId": "cd7b997c-526c-4b8f-ac56-8c6de009dc25", "code": "SYS_ACH_ENCUMBRANCE_REVERSAL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReversalCredit": { "tranCodeId": "f57ba3a2-ccfa-47de-ae81-1a8167639fe3", "code": "SYS_ACH_ENCUMBRANCE_REVERSAL_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achFeeDebit": { "tranCodeId": "d3b64ee6-0815-4d06-aa9b-906bc48cbc19", "code": "SYS_ACH_FEE_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_FEE_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_FEE_CR'", "accountId": "uuid(params.feeAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achFeeReimburseCredit": { "tranCodeId": "49b0f890-75e7-4b94-920b-54328c02b2ca", "code": "SYS_ACH_FEE_REIMBURSE_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_FEE_REIMBURSE_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_FEE_REIMBURSE_DR'", "accountId": "uuid(params.feeAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingDebit": { "tranCodeId": "e19dfe3f-6513-425a-b9a3-870f91303cc8", "code": "SYS_ACH_PENDING_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingCancelCredit": { "tranCodeId": "c66725d7-c115-4339-89e9-69b6a6ae97bd", "code": "SYS_ACH_PENDING_CANCEL_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_CANCEL_CR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_CANCEL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingCancelReversalDebit": { "tranCodeId": "f83a1768-06a1-4a6b-a472-6e9b9c7dbe75", "code": "SYS_ACH_PENDING_CANCEL_REVERSAL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_CANCEL_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_CANCEL_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingReversalDebit": { "tranCodeId": "21036f96-bc05-4526-8004-e63ff6955d0a", "code": "SYS_ACH_PENDING_REVERSAL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleCredit": { "tranCodeId": "d918eb34-1ef9-4437-a82b-46c169f63e41", "code": "SYS_ACH_SETTLE_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleDebit": { "tranCodeId": "5af51352-84ad-4534-aacf-4e68c2ed2e2b", "code": "SYS_ACH_SETTLE_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleReturnCredit": { "tranCodeId": "90e09ed8-ae9c-46f2-9be8-2e09b4a5de35", "code": "SYS_ACH_SETTLE_RETURN_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_RETURN_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_RETURN_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleReturnDebit": { "tranCodeId": "2b11b428-54ed-429c-97d9-b9a4e3ef6007", "code": "SYS_ACH_SETTLE_RETURN_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_RETURN_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_RETURN_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] } } } ``` ### Workflows In addition to the built-in tran codes, Twisp offers a number of workflows that will post & void transactions as appropriate for each stage of the ACH transaction lifecycle. These are built on top of the [executeTask](/docs/reference/graphql/mutations#workflow.execute-task) workflow invocation. Below we will look at sample graphql for invocation of the workflow and describe the various tasks available for each type of workflow. #### ODFI Push | Task | Valid From | Description | |---------------|------------------------|---------------------------------------------------------------------------------------------| | CREATE | | Debit customers account at `PENDING` layer, funds leaving in next ACH batch. | | SUBMIT | CREATE | Void the `PENDING` transaction and post settlement, funds are sent to external institution. | | RETURN | SUBMIT | External institution returned the funds for some reason. e.g. Account is closed. | | CANCEL | CREATE | Cancel the ACH transaction before the batch window is reached. | | CONTINUE | CANCEL, REIMBURSE_FEE | Undo the cancellation. | | REIMBURSE_FEE | CREATE, SUBMIT, RETURN | Reimburse optional fee to the customer. | **Request** ```graphql mutation ODFIPush( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! $feeAccountId: UUID! ) { # Push is scheduled create: workflow { executeTask( input: { task: "CREATE" workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" params: { accountId: $accountId feeAccountId: $feeAccountId # optional fee accountId for fees settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" # optional fee amount feeAmount: "0.50" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Fee can be reimbursed after CREATE # reimburse: workflow { # executeTask( # input: { # task: "REIMBURSE_FEE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # cancel will reimburse fee if not reimbursed yet # cancel: workflow { # executeTask( # input: { # task: "CANCEL" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # continue allows workflow to progress to submit # the fee will still apply depending on whether a REIMBURSE_FEE occurred # continue: workflow { # executeTask( # input: { # task: "CONTINUE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # submit means in an ach file on the way to Fed. submit: workflow { executeTask( input: { task: "SUBMIT" workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # fee can be reimbursed after SUBMIT # reimburse: workflow { # executeTask( # input: { # task: "REIMBURSE_FEE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # The external institution returned the ACH. return: workflow { executeTask( input: { task: "RETURN" workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # fee can be reimbursed after RETURN # reimburse: workflow { # execute( # input: { # task: "REIMBURSE_FEE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) # } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_PENDING_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_PENDING_CR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "0.50" }, "direction": "DEBIT", "entryType": "ACH_FEE_DR" }, { "amount": { "units": "0.50" }, "direction": "CREDIT", "entryType": "ACH_FEE_CR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" } ] } } ] } }, "submit": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_PENDING_REVERSAL_DR" }, { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_PENDING_REVERSAL_CR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` #### ODFI Pull | State | Valid From | Description | |--------|----------------|------------------------------------------------------------------------------------| | CREATE | | Credit customer account from an external account at `ENCUMBRANCE` layer. | | CANCEL | CREATE | Cancel transfer if before ACH batch cutoff. | | SUBMIT | CREATE | Submit transfer in ACH file. Post at `PENDING` layer until settlement. | | SETTLE | SUBMIT | After 3 days without return, post transfer to `SETTLED` layer. | | RETURN | SUBMIT, SETTLE | If received a return, send funds from customer account back to settlement account. | **Request** ```graphql mutation ODFIPull( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! ) { # Transfer will happen in next batch. Funds credited at ENCUMBRANCE layer create: workflow { executeTask( input: { task: "CREATE" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { accountId: $accountId settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Can cancel after create # cancel: workflow { # executeTask( # input: { # task: "CANCEL" # workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" # executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # Can undo cancellation # continue: workflow { # executeTask( # input: { # task: "CONTINUE" # workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" # executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # Transfer is in the ACH file. Funds are credited at PENDING layer. submit: workflow { executeTask( input: { task: "SUBMIT" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Invoked after three days without return. Funds are credited at SETTLED layer. settle: workflow { executeTask( input: { task: "SETTLE" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # A return is recieved, move funds back to settlement account. return: workflow { executeTask( input: { task: "RETURN" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" } ] } } ] } }, "settle": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" }, { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" } ] } } ] } }, "submit": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" }, { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` #### RDFI Credit | State | Valid From | Description | |--------|----------------|-----------------------------------------------------------------| | CREATE | | Creates an encumbrance credit, funds will deposit into account. | | SETTLE | CREATE | Funds are now settled. | | RETURN | SETTLE, CREATE | Returned funds to originating institution. | **Request** ```graphql mutation RDFICredit( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! ) { # ACH is in file effective for future, put in PENDING layer create: workflow { executeTask( input: { task: "CREATE" workflowId: "44d71180-6db2-437e-9083-6ba672f41ba2" executionId: "779b773c-f7a3-41f3-905b-5934dcda8932" params: { accountId: $accountId settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Reached the effective date, put into SETTLED layer. settle: workflow { executeTask( input: { task: "SETTLE" workflowId: "44d71180-6db2-437e-9083-6ba672f41ba2" executionId: "779b773c-f7a3-41f3-905b-5934dcda8932" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Initiate a return of funds to the settlement account. return: workflow { executeTask( input: { task: "RETURN" workflowId: "44d71180-6db2-437e-9083-6ba672f41ba2" executionId: "779b773c-f7a3-41f3-905b-5934dcda8932" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" } ] } } ] } }, "settle": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" }, { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` #### RDFI Debit | State | Valid From | Description | |--------|----------------|---------------------------------------------------------------| | CREATE | | Creates and encumbrance debit, funds will debit from account. | | SETTLE | CREATE | Funds are now settled. | | RETURN | CREATE, SETTLE | Initiated a return, funds credit back to customer account. | **Request** ```graphql mutation RDFIDebit( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! ) { create: workflow { executeTask( input: { task: "CREATE" workflowId: "c9085474-0b55-4bda-a279-04535d7cb8b7" executionId: "e3271c9d-cdcf-4f38-af96-a3916219f611" params: { accountId: $accountId settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } settle: workflow { executeTask( input: { task: "SETTLE" workflowId: "c9085474-0b55-4bda-a279-04535d7cb8b7" executionId: "e3271c9d-cdcf-4f38-af96-a3916219f611" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } return: workflow { executeTask( input: { task: "RETURN" workflowId: "c9085474-0b55-4bda-a279-04535d7cb8b7" executionId: "e3271c9d-cdcf-4f38-af96-a3916219f611" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" } ] } } ] } }, "settle": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" }, { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` ## Clearing Settlements The cash account is an important one for double entry accounting. This is account is the mechanism for getting money from the outside world into and out of your system. In our case there are several accounts that may interact with the outside world and change the total cash position of the system: - Settlement Accounts - Cards - ACH - Revenue accounts - Disputes and other operational accounts These accounts will be "cleared" in bulk with the aggregate amount at the time of clearing. Let's say you have $2M in card debits on a particular day across 2 transactions. Where your starting cash balance is $100M | DR | CR | Balances | |--------------------|-----------------|-----------------------------| | $2M Card Settle | $2M Cash | Card Settle ($2M), Cash $98 | | $1M Deposit Acct 1 | $1M Card Settle | Card Settle($1M) | | $1m Deposit Acct 2 | $1M Card Settle | Card Settle $0 | This bulk clearing to get funds on/off the platform should be conducted with it's own tran codes. These settlement accounts are often backed by an account at a bank, and so the balances of these accounts should be reconciled with your representation of them in your accounting system. **Request** ```graphql mutation ClearingTranCode($clearing: TranCodeInput!) { createTranCode(input: $clearing) { ...TC } } fragment TC on TranCode { tranCodeId code description params { name type default description } transaction { effective journalId correlationId externalId description metadata } entries { entryType accountId layer direction units currency description metadata condition } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "2cc6935d-ca90-40a6-8420-b9054775d6b8", "code": "CLEARING", "description": "Clear funds between cash and settlement accounts.", "params": [ { "name": "settlementAccount", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "cashAccount", "type": "UUID", "default": "db07e5cf-6cd7-4629-a952-9613578cd8ea", "description": "The cash account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The amount to clear." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the clearance." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of clearing. DEBIT raises cash account balance. CREDIT lowers." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." }, { "name": "description", "type": "STRING", "default": "Clearing settlement.", "description": "describe this transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "params.description", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CLEARING_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.cashAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null }, { "entryType": "'CLEARING_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null } ] } } } ``` **Variables** ```json { "clearing": { "tranCodeId": "2cc6935d-ca90-40a6-8420-b9054775d6b8", "code": "CLEARING", "description": "Clear funds between cash and settlement accounts.", "params": [ { "name": "settlementAccount", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "cashAccount", "type": "UUID", "default": "db07e5cf-6cd7-4629-a952-9613578cd8ea", "description": "The cash account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The amount to clear." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the clearance." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of clearing. DEBIT raises cash account balance. CREDIT lowers." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." }, { "name": "description", "type": "STRING", "default": "Clearing settlement.", "description": "describe this transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "params.description", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CLEARING_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.cashAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null }, { "entryType": "'CLEARING_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null } ] } } ``` ## Advanced Topics As we've seen, Tran Codes are a powerful abstraction to encapsulate accounting concerns inside of Twisp. We also explored one workflow that Twisp offers for ACH transactions, to automatically handle posting and voiding transactions to a customers account for each part of an ACH lifecycle. Twisp offers several other workflows that may be useful for interacting with the accounting core. In this section we'll briefly examine each one. ### Void And Post Workflow The Void and Post workflow is a powerful mechanism for modeling a transaction that may change layers and amounts over time. In essence a single transaction identifier, the `executionId` passed to the workflow, will void the prior transaction created by the workflow and create a new transaction with the tran code you've provided to the workflow. This allows for some powerful transaction modeling. Consider the earlier section on card holds. In that you may be composing together two graphql queries: - `voidTransaction` to void the "prior authorization" - `postTransaction` to post the new authorization value With the void and post workflow, we can use a single identifier for the "authorization" and update it without needing to keep track of multiple transaction ids. Consider this example which posts an authorization and then updates it. **Request** ```graphql mutation VoidAndPost( $authorizationId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $updatedAmount: Decimal! $effective: Date ) { originalAuth: workflow { executeTask( input: { workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4" executionId: $authorizationId task: "VOID_AND_POST" params: { tranCode: "CARD_HOLD" account: $accountId amount: $amount correlation: $correlation effective: $effective } } ) { transactions { transactionId entries(first: 10) { nodes { entryType direction amount { units } } } } } } updatedAuth: workflow { executeTask( input: { workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4" executionId: $authorizationId task: "VOID_AND_POST" params: { tranCode: "CARD_HOLD" account: $accountId amount: $updatedAmount correlation: $correlation effective: $effective } } ) { transactions { transactionId entries(first: 10) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "originalAuth": { "executeTask": { "transactions": [ { "transactionId": "edcc3942-3c4f-5d40-8fa7-0a2cbc068631", "entries": { "nodes": [ { "entryType": "CARD_HOLD_DR", "direction": "DEBIT", "amount": { "units": "10.00" } }, { "entryType": "CARD_HOLD_CR", "direction": "CREDIT", "amount": { "units": "10.00" } } ] } } ] } }, "updatedAuth": { "executeTask": { "transactions": [ { "transactionId": "fed601a6-ce71-5760-8dbc-5ec5e392ff29", "entries": { "nodes": [ { "entryType": "CARD_HOLD_DR_VOID", "direction": "DEBIT", "amount": { "units": "-10.00" } }, { "entryType": "CARD_HOLD_CR_VOID", "direction": "CREDIT", "amount": { "units": "-10.00" } } ] } }, { "transactionId": "bdcdac07-29fc-5f5f-9e65-9b9a37241c9c", "entries": { "nodes": [ { "entryType": "CARD_HOLD_DR", "direction": "DEBIT", "amount": { "units": "15.00" } }, { "entryType": "CARD_HOLD_CR", "direction": "CREDIT", "amount": { "units": "15.00" } } ] } } ] } } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "updatedAmount": "15.00", "authorizationId": "a60f3df6-a1e8-4c04-bac2-bc9d15e94bc3", "correlation": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "effective": "2022-10-01" } ``` ### States Language Workflow GraphQL is an extremely expressive querying language, however sometimes it requires multiple interactions to serve a request. Take for example the card authorization use case: - Optimistically post transaction - Check if any balances are violated - Void if one of the balances is violated - Indicate in response if transaction is Approved or Declined There could possibly be 2-3 network calls to Twisp to fulfill this use case with just the GraphQL api interacting with your application. However, if you want to accomplish all of this _transactionally_ in Twisp, we support defining state machines using [States Language](https://states-language.net/) that allows you to cooridnate multiple api calls. In the following example: - post a `CARD_HOLD` optimistically to a card account - check the balance of the parent deposit account set - if the balance is less than zero we void - return either a `Declined` or `Approved` message based on the balance > **Tip:** > > This feature is not yet GA but we're looking to ship in the next few days. **Request** ```graphql mutation PostCheckAndVoid( $authorizationVariables: JSON! $machine: StateMachine! ) { workflow { executeStatesLanguage( input: { input: $authorizationVariables, machine: $machine } ) { output } } } ``` **Response** ```json { "data": { "workflow": { "executeStatesLanguage": { "output": { "message": "Declined", "variables": { "balance": "-291.00", "hasBalance": true, "shouldVoid": true, "transactionId": "772269e7-4c11-4375-bc3d-587fed54b2ea", "voided": { "voidTransaction": { "transactionId": "a97b893f-cf69-54f8-ac51-afa5d6f789ff" } } } } } } } } ``` **Variables** ```json { "machine": { "Id": "c4edf3bd-ae11-490f-98bc-f3d9547bc516", "Name": "PostAndVoid", "AwsStatesLanguage": { "Comment": "Post a transaction and void if balance is lt zero.", "StartAt": "Post", "States": { "Post": { "Type": "Task", "Resource": "arn:twisp:workflow:::financial/v1/graphql", "Parameters": { "query": "mutation Auth(\n $authorizationId: UUID!\n $accountId: UUID!\n $correlation: String\n $amount: Decimal!\n $effective: Date\n){\n postTransaction(\n input:{\n transactionId: $authorizationId\n tranCode: \"CARD_HOLD\"\n params: {\n account: $accountId\n amount: $amount\n correlation: $correlation\n effective: $effective\n metadata: \"{}\"\n }\n }\n) {\n transactionId\n}}\n", "variables": { "authorizationId.$": "$.authorizationId", "accountId.$": "$.accountId", "amount.$": "$.amount", "correlation.$": "$.correlation", "effective.$": "$.effective" } }, "ResultSelector": { "transactionId.$": "$.postTransaction.transactionId" }, "ResultPath": "$.variables.posted", "Next": "CheckBalance" }, "CheckBalance": { "Type": "Task", "Resource": "arn:twisp:workflow:::financial/v1/graphql", "Parameters": { "query": "query CheckBalance($accountId: UUID!) {\n account(\n id: $accountId\n ) {\n sets(first:1) {\n nodes {\n sets(first:1) {\n nodes {\n balance {\n available(layer:PENDING) {\n normalBalance {\n units\n }\n }\n }\n }\n }\n }\n }\n }}\n", "variables.$": "$.variables" }, "ResultPath": "$.balance", "Next": "FormatResult" }, "FormatResult": { "Type": "Task", "Resource": "arn:twisp:workflow:::cel", "Parameters": { "variables": { "hasBalance": "size(context.workflows.this.CheckBalance.balance.account.sets.nodes) > 0 && size(context.workflows.this.CheckBalance.balance.account.sets.nodes[0].sets.nodes) > 0", "balance": "this.hasBalance ? context.workflows.this.CheckBalance.balance.account.sets.nodes[0].sets.nodes[0].balance.available.normalBalance.units : decimal('0')", "shouldVoid": "decimal(this.balance) <= decimal('0')", "transactionId": "context.workflows.this.CheckBalance.variables.posted.transactionId" } }, "Next": "ShouldVoid" }, "ShouldVoid": { "Type": "Choice", "Default": "Approved", "Choices": [ { "Variable": "$.variables.shouldVoid", "BooleanEquals": true, "Next": "Void" } ] }, "Void": { "Type": "Task", "Resource": "arn:twisp:workflow:::financial/v1/graphql", "Parameters": { "query": "mutation VoidTransaction($transactionId: UUID!) {\n voidTransaction(id: $transactionId) { transactionId }\n}\n", "variables.$": "$.variables" }, "ResultPath": "$.variables.voided", "Next": "Declined" }, "Approved": { "Type": "Pass", "Parameters": { "message": "Approved", "variables.$": "$.variables" }, "End": true }, "Declined": { "Type": "Pass", "Parameters": { "message": "Declined", "variables.$": "$.variables" }, "End": true } } } }, "authorizationVariables": { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "amount": "10.00", "authorizationId": "772269e7-4c11-4375-bc3d-587fed54b2ea", "correlation": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "effective": "2022-10-01" } } ``` ### Custom Balance Computations By default Twisp rolls up balances on a number of dimensions: - journal - account - currency Twisp offers the ability to compute balances on additional dimensions. For example, often fintechs will put limits in per MCC code or perhaps on daily, weekly, monthly or yearly spending limits. These are supported in Twisp via [Balance Calculations](/docs/reference/graphql/mutations#create-calculation). Once you've created a balance calcuation, you may look up balance on that calculation via the balances endpoint by supplying the `calculationId` and `dimension` values you're interested in. For example, for the above calculation: ```graphql query GetJan12020EffectiveBalance($accountId: UUID!) { balance( accountId: $accountId currency: "USD" calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" dimension: { effectiveDate: "2020-01-01" } ) { available(layer:PENDING) { normalBalance { units } } } } ``` --- # Position-Based Accounting for Securities Trading with Twisp Learn how to set up position-based accounting for securities trading using Twisp in this comprehensive guide. From designing a suitable chart of accounts and defining transaction codes, to posting transactions for trading activities and analyzing performance over time, this guide provides step-by-step instructions for managing the complexities of securities trading. Ideal for those in financial institutions or investment companies looking to streamline their securities accounting process. Source: https://www.twisp.com/docs/guides/position-accounting > **Task:** > > Over the course of this guide, we will: > - Design a chart of accounts > - Define tran code for securities transactions > - Post transactions for securities trading activities > - Query balances and transaction history ## Design the Chart of Accounts To set up position-based accounting for securities trading with Twisp, you'll need to create a chart of [accounts](/docs/reference/graphql/types/object#account) that can accurately represent the financial activities associated with trading securities. In this section we'll walk through the initial steps needed to set up your ledger. To create these accounts, refer to the account creation process outlined in the Twisp documentation (source: [Creating Accounts](/docs/accounting-core/chart-of-accounts#creating-accounts)). ### 1. Create securities trading accounts for customers To represent each customer's securities trading activities, you'll need to create separate accounts for them within your chart of accounts. Because accounts can maintain balances across multiple currencies, we can treat each security as its own currency and thus a single customer account can hold positions across many securities. A simple way to set this up would use a single account for each customer, e.g. `CLIENT_A`, `CLIENT_B`, etc. A more robust setup might use an account set for each customer, with sub-accounts for different purposes: - `CLIENT_*.CASH` - (asset) debit-normal - `CLIENT_*.INVESTMENT` - (equity) credit-normal - `CLIENT_*.HOLDINGS` - (asset) debit-normal - `CLIENT_*.DIVIDEND_REVENUE` - (revenue) credit-normal ### 2. Establish revenue and expense accounts for trading fees and transaction costs In order to account for trading fees and transaction costs associated with securities trading, you'll need to create separate revenue and expense accounts within your chart of accounts. This will enable you to accurately track fees charged to customers and expenses incurred by your business for executing trades. ### 3. Add accounts for realized and unrealized gains or losses Lastly, you'll need to create accounts for tracking realized and unrealized gains or losses resulting from securities trading activities. Realized gains or losses occur when a security is sold, while unrealized gains or losses represent the change in value of securities that are still being held by customers. By creating separate accounts for these financial events, you can effectively monitor the performance of your customers' portfolios and provide accurate financial reporting. ## Define Transaction Codes for Securities Transactions In this section, we will design the transaction codes ([tran codes](/docs/reference/graphql/types/object#tran-code)) for various securities transactions, such as buying and selling securities, dividend and interest payments, and handling fees and transaction costs. ### 1. Design transaction codes for buying securities To create a transaction code for buying securities, follow these steps: - Define a unique identifier code for this tran code, such as `BUY_SECURITY`. - Determine the accounts that will be debited and credited. For example, debit the customer's cash account for the purchase amount and credit the customer's securities account for the purchased security. - Define the entry data to be written for each entry, such as entry types like `BUY_SECURITY_DR` and `BUY_SECURITY_CR`. - Parameterize the inputs, including the purchase amount, security type, and customer account IDs. For example, this tran code might accept the following parameters: - `clientHoldingsAcct` (`UUID`) - `clientInvestAcct` (`UUID`) - `clientCashAcct` (`UUID`) - `security` (`STRING`) - `shares` (`DECIMAL`) - `price` (`DECIMAL`) - `feeRate` (`DECIMAL`) - `effective` (`DATE`) The exact nature of your ledger will dictate how entries should be written. An example set of entries for this tran code could be: | Entry Type | Account | Units | Currency | Direction | Layer | |---------------------------------|----------------------|------------------------------------|------------|-----------|---------| | BUY_SECURITY_PAYMENT_CR | `clientCashAcct` | `(price * shares) * (1 + feeRate)` | USD | CREDIT | SETTLED | | BUY_SECURITY_PAYMENT_DR | `clientInvestAcct` | `(price * shares) * (1 + feeRate)` | USD | DEBIT | SETTLED | | BUY_SECURITY_BROKER_EARNINGS_CR | BROKER.REVENUE | `(price * shares)` | USD | CREDIT | SETTLED | | BUY_SECURITY_BROKER_FEE_CR | BROKER.REVENUE | `(price * shares) * (feeRate)` | USD | CREDIT | SETTLED | | BUY_SECURITY_BROKER_EARNINGS_DR | BROKER.SETTLEMENT | `(price * shares) * (1 + feeRate)` | USD | DEBIT | SETTLED | | BUY_SECURITY_TRADE_CR | BROKER.HOLDINGS | `shares` | `security` | CREDIT | SETTLED | | BUY_SECURITY_TRADE_DR | `clientHoldingsAcct` | `shares` | `security` | DEBIT | SETTLED | Let's break these entries down. 1. **BUY_SECURITY_PAYMENT_CR**: This is the entry for the payment made by the client for purchasing the security. The account debited is the `clientCashAcct` which is the account where the client holds cash. The amount is calculated by multiplying the price per share of the security by the number of shares being purchased. This total is then adjusted upwards for the brokerage fee by multiplying by `(1 + feeRate)`. The direction is CREDIT because money is leaving this account to make the payment. The entry is marked as SETTLED, meaning the transaction is complete. 2. **BUY_SECURITY_PAYMENT_DR**: This is the mirror entry to the above, posting to the `clientInvestAcct` (Client Investment Account) which represents the client's investments in securities. The amount is the same as in the `BUY_SECURITY_PAYMENT_CR` entry, but the direction is DEBIT because the account is receiving value in the form of the purchased security. 3. **BUY_SECURITY_BROKER_EARNINGS_CR**: This entry records the revenue earned by the broker from the client's purchase. The account credited is `BROKER.REVENUE`. The amount is the raw cost of the security purchase without any fee adjustment. 4. **BUY_SECURITY_BROKER_FEE_CR**: This entry records the brokerage fee that the client paid for this transaction. The account credited is again `BROKER.REVENUE`. The amount is the cost of the security purchase multiplied by the fee rate. 5. **BUY_SECURITY_BROKER_EARNINGS_DR**: This entry records the total amount of the purchase (including fee) as a debit to `BROKER.SETTLEMENT` account. It is a mirror to the `BUY_SECURITY_BROKER_EARNINGS_CR` and `BUY_SECURITY_BROKER_FEE_CR` entries combined, effectively 'settling' the broker's accounts. 6. **BUY_SECURITY_TRADE_CR**: This entry records the receipt of the purchased shares in the `BROKER.HOLDINGS` account. The amount is the number of shares bought, and the currency column indicates the type of security purchased. 7. **BUY_SECURITY_TRADE_DR**: This is the mirror entry to the above, recording the departure of the purchased shares from the `clientHoldingsAcct`. The amount is the number of shares bought. Overall, these entries reflect the flow of money and securities between the client and the broker during a `BUY_SECURITY` transaction. They ensure the integrity and correctness of the accounting process by adhering to the double-entry accounting system, where each transaction is represented by two or more entries that balance out. ### 2. Design transaction codes for selling securities To create a transaction code for selling securities, follow these steps: - Define a unique identifier code for this tran code, such as `SELL_SECURITY`. - Determine the accounts that will be debited and credited. For example, debit the customer's securities account for the sold security and credit the customer's cash account for the sale proceeds. - Define the entry data to be written for each entry, such as entry types like `SELL_SECURITY_DR` and `SELL_SECURITY_CR`. - Parameterize the inputs, including the sale amount, security type, and customer account IDs. ### 3. Develop transaction codes for dividend or interest payments To create a transaction code for dividend or interest payments, follow these steps: - Define a unique identifier code for this tran code, such as `DIVIDEND_PAYMENT` or `INTEREST_PAYMENT`. - Determine the accounts that will be debited and credited. For example, debit the asset account representing dividends or interest payments and credit the customer's cash account for the payment received. - Define the entry data to be written for each entry, such as entry types like `DIV_PAYMENT_DR` and `DIV_PAYMENT_CR` or `INT_PAYMENT_DR` and `INT_PAYMENT_CR`. - Parameterize the inputs, including the payment amount and customer account IDs. By defining appropriate transaction codes for various securities transactions, you can ensure that your securities trading platform maintains a consistent, predictable, and correct ledger using Twisp's powerful accounting features. ## Post Transactions for Securities Trading Activities Now that we have the accounts and tran codes defined, we can start using them to post transactions. Use the transaction codes you created in the previous section for buying and selling securities. ### Example: buying a security As an example, let's say we wanted to record a `BUY_SECURITY` transaction where `CLIENT_A` bought 100 shares of AAPL at $149.45 pre share with a 0.75% brokerage fee, effective as of October 12, 2022. The values of the params would be: - `clientHoldingsAcct`: UUID for `CLIENT_A.HOLDINGS` account - `clientInvestAcct`: UUID for `CLIENT_A.INVESTMENT` account - `clientCashAcct`: UUID for `CLIENT_A.CASH` account - `security`: `'AAPL'` - `shares`: `100` - `price`: `149.45` - `feeRate`: `0.0075` - `effective`: `'2022-10-12'` After posting, the following entries would be written to the ledger: | Entry Type | Account | Units | Currency | Direction | Layer | |---------------------------------|---------------------|------------|----------|-----------|---------| | BUY_SECURITY_PAYMENT_CR | CLIENT_A.CASH | 15057.0875 | USD | CREDIT | SETTLED | | BUY_SECURITY_PAYMENT_DR | CLIENT_A.INVESTMENT | 15057.0875 | USD | DEBIT | SETTLED | | BUY_SECURITY_BROKER_EARNINGS_CR | BROKER.REVENUE | 14945 | USD | CREDIT | SETTLED | | BUY_SECURITY_BROKER_FEE_CR | BROKER.REVENUE | 112.0875 | USD | CREDIT | SETTLED | | BUY_SECURITY_BROKER_EARNINGS_DR | BROKER.SETTLEMENT | 15057.0875 | USD | DEBIT | SETTLED | | BUY_SECURITY_TRADE_CR | BROKER.HOLDINGS | 100 | AAPL | CREDIT | SETTLED | | BUY_SECURITY_TRADE_DR | CLIENT_A.HOLDINGS | 100 | AAPL | DEBIT | SETTLED | ### Post additional transactions to exercise all tran codes Try posting more transactions for each of your tran code types. After posting, query the entries written to confirm that they correspond with what you expect. - Account for dividend or interest payments - Apply fees and transaction costs to the relevant accounts - Calculate and record realized and unrealized gains or losses ## Monitor and Analyze Securities Trading Activities In this section, we will cover how to monitor and analyze securities trading activities. These steps will help you ensure accuracy, maintain regulatory compliance, and track your customers' portfolios. ### 1. Query account balances and transaction history for individual customers Using Twisp's GraphQL API, you can query the account balances and transaction history for each customer's securities trading accounts. This will enable you to track the financial activity of your customers and identify any potential issues or discrepancies. ### 2. Track portfolio performance and valuation over time To track the performance and valuation of your customers' portfolios, you can use Twisp's layering system to create snapshots of balances at different points in time. By comparing these snapshots, you can calculate the changes in the value of your customers' portfolios and analyze their performance over specific periods. ### 3. Review and audit securities trading activities Twisp's immutable ledger records and versioned account history allow you to easily audit and verify all securities trading activities. By regularly reviewing the transaction records and account histories, you can ensure the accuracy of your accounting system, identify possible fraud or misuse, and maintain regulatory compliance. ### 4. Generate financial reports and statements for regulatory compliance and management purposes Using the data stored in Twisp's ledger, you can generate various financial reports and statements required for regulatory compliance, as well as for internal management purposes. These reports can help you monitor the overall performance of your securities trading business and make informed decisions based on accurate financial data. ## Conclusion After following this guide, you should now be proficient in setting up position-based accounting for securities trading with Twisp. We started by designing an effective chart of accounts and defining precise transaction codes for various securities activities. Following this, we explored how to post transactions for these activities and how to accurately account for all the associated costs and gains. Finally, we discussed how to monitor and analyze trading activities to provide valuable insights into portfolio performance and compliance with regulatory requirements. By utilizing the functionalities of Twisp in your securities trading, you can ensure a streamlined and efficient accounting process. Moreover, the ability to monitor and analyze your trading activities in real-time provides an added level of control, enabling you to make data-driven decisions and optimize performance. Remember that continuous improvement is key to maintaining a robust and efficient accounting system. Regularly review your setup, adjust transaction codes and accounts when necessary, and keep an eye on new Twisp features and updates that could further improve your securities accounting process. Thank you for choosing Twisp as your partner in financial management. Happy trading! --- # Processing ACH Payments Process production ACH payments efficiently Source: https://www.twisp.com/docs/guides/processing-ach-payments ## Coming Soon --- # Reconciling ACH Files Validate and reconcile ACH file processing Source: https://www.twisp.com/docs/guides/reconciling-ach-files ## Coming Soon --- # Modeling Banking on Twisp Design and Implement bank-like core accounting on Twisp. Source: https://www.twisp.com/docs/guides/unit-integration ## Context Organizations that offer financial products as part of their core product offering have a wide variety of service providers to bring products to market. There are vertical banking integrations such as Unit, which provide turn key treasury, lending and card operations. Other companies are more specialized, card processors like Marqeta and Lithic and ACH and payments companies like Sila and Stripe. As a company building on top of these service providers you're often going through a "Crawl, Walk, Run" maturation cycle: ```mermaid timeline title Maturation Cycle of Fintech Crawl : Lean on BaaS providers : Shallow banking relationships : Use case limited Walk : Use specialized processors : Deep banking relationships : Innovation unlocked Run : In house capability : Becoming a bank feasible : Highest leverage ``` The goal as a fintech is to prove your product and get to the "Run" stage as fast as possible. One of the first systems you'll need to build and operate well is a core account system. Twisp is a core accounting system designed to provide a system of record for organizations that build financial products and services. In this document we'll dive into how to start modeling deposit accounts, credit accounts and their interaction with various payment instruments, which you'll find applicable to any stage of development. ## Scope In this document we'll cover: 1. Deposit and Credit accounts 3. Card transactions 4. ACH We will design a number of "system level" accounts for operational and double entry accounting purposes. And we'll build out sample chart of accounts for a business neobanking vertical that we can iterate on toward your use case. ## Chart of Accounts The chart of accounts in your fintech system is the building block for how you want balances to "roll up" for both end users and for your platform. It is helpful to think of these charts as separate ones that interact with each other when funds are spent: 1. **End User**: Track end user activity and control how balances "roll up" for end users of our system. 2. **Platform**: Track settlements, revenue and operational concerns of the platform. ### End User We're going to cover two basic kinds of accounts: - **Deposit Accounts**: DDA accounts are stores of value for banking customers. Debit instruments are hooked up to these accounts and these accounts are considered liabilities to the platform, because the platform owes the deposits to the account holders. - **Credit Accounts**: Credit accounts are accounts with a line of credit (as we'll see later on, literally a line on an account) coupled with a payable account. Customers will spend money and then they owe the account issuer funds back by a certain date, and the payable account accrues interest. Twisp models both accounts similarly: - An account set to represent the total balance of the account - A default account to represent debits/credits incurred by the account holder The difference between the two will be the [Balance Normality](/docs/accounting-core/chart-of-accounts#credit-normal-and-debit-normal) of the accounts involved, and the credit accounts will have an extra account to hold the credit line. #### Deposit Accounts The chart of accounts for end users will model a `Customer -> Account -> Card` hiearchical relationship. This hierarchical relationship we'll model via [Account Sets](/docs/reference/graphql/types/object#account-set). Each individual entity we'll create via a two objects as a building block for creating the chart of accounts: 1. An [Account Set](/docs/reference/graphql/types/object#account-set) to encapsulate the _total balance_ of the entity in question. 2. A default [Account](/docs/reference/graphql/types/object#account) as a member of the above account set for writing entries to the entity. ```mermaid graph BT SET[/Entity Account Set\] DEFAULT[/Entity Default Account\] DEFAULT --> SET ``` This building block can be encapsulated in via a GraphQL mutation: **Request** ```graphql mutation CreateEntityAccount( $accountSetId: UUID! $accountId: UUID! $code: String! $description: String $metadata: JSON $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $accountSetName: String! = "Entity Account Set" $accountName: String! = "Entity Default Account" ) { createAccountSet( input: { accountSetId: $accountSetId name: $accountSetName description: $description metadata: $metadata journalId: $journalId } ) { accountSetId } createAccount( input: { accountId: $accountId name: $accountName description: $description code: $code accountSetIds: [$accountSetId] metadata: $metadata } ) { accountId } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "37279a24-976e-4a16-805d-953245e5606f" }, "createAccount": { "accountId": "08ed9947-6775-47a7-81d3-b1cd84f4455a" } } } ``` **Variables** ```json { "accountSetId": "37279a24-976e-4a16-805d-953245e5606f", "accountId": "08ed9947-6775-47a7-81d3-b1cd84f4455a", "code": "37279a24-976e-4a16-805d-953245e5606f.DEFAULT", "description": "Account for Entity", "metadata": {} } ``` Once we have this building block, we can now model the chart of accounts creating and adding to the appropriate account set. Consider the use case of onboarding a customer, followed by creating a deposit account and issuing a debit card; After onboarding we'd expect to have the following end user chart of accounts: ```mermaid graph BT CUST[/Customer Account Set\] CUST_DEF[/Default Customer Account\] DEPOSIT[/Deposit Account Set\] DEPOSIT_DEF[/Default Deposit Account\] CARD[/Card Account Set\] CARD_DEF[/Default Card Account\] CUST_DEF & DEPOSIT --> CUST DEPOSIT_DEF & CARD --> DEPOSIT CARD_DEF --> CARD ``` The coordination of creating an account in Twisp can occur in many ways. For example, it may be in response to webhooks from a BaaS provider: | Webhook Received | Actions in Twisp | |--------------------|------------------------------------------------------------| | `customer.created` | Create customer account | | `account.created` | Create deposit account, add to customer account | | `card.created` | Create card account, add to corresponding deposit account. | Another might be creating Twisp entities via a state machine process, such as Temporal or Step Functions, coordinating activity with a number of vendors: **Request** ```graphql # OnboardCustomerDepositAccountCard does exactly that: # - Creates Account Set and Default Account for Customer # - Creates Account Set and Default Account for Deposit Account # - Adds Deposit Account to Customer # - Creates Account Set and Default Account for Card # - Adds Card to Deposit Account # This includes adding Unit metadata json. This can be # broken up into multiple mutations in response to a webhook # or used as-is in some kind of long running coordinated process # to onboard a customer. mutation OnboardCustomerDepositAccountCard( $customerAccountSetId: UUID! $customerAccountId: UUID! $customerAccountCode: String! $depositAccountSetId: UUID! $depositAccountId: UUID! $depositAccountCode: String! $cardAccountSetId: UUID! $cardAccountId: UUID! $cardAccountCode: String! $description: String $customerMetadata: JSON $depositMetadata: JSON $cardMetadata: JSON $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $customerAccountSetName: String! = "Customer Account Set" $customerAccountName: String! = "Customer Default Account" $depositAccountSetName: String! = "Deposit Account Set" $depositAccountName: String! = "Deposit Default Account" $cardAccountSetName: String! = "Card Account Set" $cardAccountName: String! = "Card Default Account" ) { # Onboard Customer customerSet: createAccountSet( input: { accountSetId: $customerAccountSetId name: $customerAccountSetName description: $description metadata: $customerMetadata journalId: $journalId } ) { accountSetId } customerDefault: createAccount( input: { accountId: $customerAccountId name: $customerAccountName description: $description code: $customerAccountCode accountSetIds: [$customerAccountSetId] metadata: $customerMetadata } ) { accountId } # Onboard Deposit Account depositSet: createAccountSet( input: { accountSetId: $depositAccountSetId name: $depositAccountSetName description: $description metadata: $depositMetadata journalId: $journalId } ) { accountSetId } depositDefault: createAccount( input: { accountId: $depositAccountId name: $depositAccountName description: $description code: $depositAccountCode metadata: $depositMetadata } ) { accountId } depositToCustomer: addToAccountSet( id: $customerAccountSetId member: { memberType: ACCOUNT_SET, memberId: $depositAccountSetId } ) { accountSetId } defaultDepositToSet: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT, memberId: $depositAccountId } ) { accountSetId } # Onboard Card cardSet: createAccountSet( input: { accountSetId: $cardAccountSetId name: $cardAccountSetName description: $description metadata: $cardMetadata journalId: $journalId } ) { accountSetId } cardDefault: createAccount( input: { accountId: $cardAccountId name: $cardAccountName description: $description code: $cardAccountCode metadata: $cardMetadata } ) { accountId } cardToDeposit: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT_SET, memberId: $cardAccountSetId } ) { accountSetId } defaultCardToSet: addToAccountSet( id: $cardAccountSetId member: { memberType: ACCOUNT, memberId: $cardAccountId } ) { accountSetId } } ``` **Response** ```json { "data": { "customerSet": { "accountSetId": "6669bc2c-84c1-4b76-9985-a33adeb00eae" }, "customerDefault": { "accountId": "ee5a7fa7-f336-4f70-b7e8-60473562e179" }, "depositSet": { "accountSetId": "80659068-3ac1-452a-b2da-2566e95283f8" }, "depositDefault": { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1" }, "depositToCustomer": { "accountSetId": "6669bc2c-84c1-4b76-9985-a33adeb00eae" }, "defaultDepositToSet": { "accountSetId": "80659068-3ac1-452a-b2da-2566e95283f8" }, "cardSet": { "accountSetId": "f141c872-7c66-4e6e-982c-fa1c45d94766" }, "cardDefault": { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9" }, "cardToDeposit": { "accountSetId": "80659068-3ac1-452a-b2da-2566e95283f8" }, "defaultCardToSet": { "accountSetId": "f141c872-7c66-4e6e-982c-fa1c45d94766" } } } ``` **Variables** ```json { "customerAccountSetId": "6669bc2c-84c1-4b76-9985-a33adeb00eae", "customerAccountId": "ee5a7fa7-f336-4f70-b7e8-60473562e179", "customerAccountCode": "6669bc2c-84c1-4b76-9985-a33adeb00eae.DEFAULT", "customerMetadata": { "type": "businessCustomer", "id": "1742784", "attributes": { "createdAt": "2024-03-06T18:53:48.431Z", "name": "Michael Parsons", "address": { "street": "Street name 1", "city": "City", "state": "CA", "postalCode": "11111", "country": "US" }, "phone": { "countryCode": "1", "number": "5555555555" }, "stateOfIncorporation": "CA", "ein": "123456789", "entityType": "Corporation", "contact": { "fullName": { "first": "Michael", "last": "Parsons" }, "email": "michael@twisp.com", "phone": { "countryCode": "1", "number": "5555555555" } }, "tags": {}, "authorizedUsers": [], "status": "Active" }, "relationships": { "org": { "data": { "type": "org", "id": "4975" } } } }, "depositAccountSetId": "80659068-3ac1-452a-b2da-2566e95283f8", "depositAccountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "depositAccountCode": "80659068-3ac1-452a-b2da-2566e95283f8.DEFAULT", "depositMetadata": { "id": "2890313", "type": "depositAccount", "attributes": { "hold": 0, "name": "Michael Parsons", "tags": { "purpose": "checking" }, "status": "Open", "balance": 0, "currency": "USD", "available": 0, "createdAt": "2024-03-13T20:47:56.985Z", "updatedAt": "2024-03-13T20:47:56.985Z", "routingNumber": "812345678", "depositProduct": "checking" }, "relationships": { "org": { "data": { "id": "4975", "type": "org" } }, "bank": { "data": { "id": "1", "type": "bank" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } }, "cardAccountSetId": "f141c872-7c66-4e6e-982c-fa1c45d94766", "cardAccountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "cardAccountCode": "f141c872-7c66-4e6e-982c-fa1c45d94766.DEFAULT", "cardMetadata": { "id": "1790431", "type": "businessDebitCard", "attributes": { "bin": "424242459", "tags": {}, "email": "richard@piedpiper.com", "phone": { "number": "5555555555", "countryCode": "1" }, "status": "Inactive", "address": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" }, "fullName": { "last": "Hendricks", "first": "Richard" }, "createdAt": "2024-03-13T20:56:00.442Z", "dateOfBirth": "2001-08-10", "last4Digits": "2860", "expirationDate": "2028-03", "shippingAddress": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" } }, "relationships": { "account": { "data": { "id": "2890313", "type": "account" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } }, "description": "Onboard customer, deposit account and card" } ``` This can be broken down into reusable pieces in case you want to add multiple deposit accounts or cards to a particular customer: **Request** ```graphql mutation CreateCustomer( $customerAccountSetId: UUID! $customerAccountId: UUID! $customerAccountCode: String! $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $customerAccountSetName: String! = "Customer Account Set" $customerAccountName: String! = "Customer Default Account" $customerMetadata: JSON $description: String = "Create a customer." ) { customerSet: createAccountSet( input: { accountSetId: $customerAccountSetId name: $customerAccountSetName description: $description metadata: $customerMetadata journalId: $journalId } ) { accountSetId } customerDefault: createAccount( input: { accountId: $customerAccountId name: $customerAccountName description: $description code: $customerAccountCode accountSetIds: [$customerAccountSetId] metadata: $customerMetadata } ) { accountId } } ``` **Response** ```json { "data": { "customerSet": { "accountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0" }, "customerDefault": { "accountId": "91378e87-7a08-403b-ae47-ad46f7385ede" } } } ``` **Variables** ```json { "customerAccountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0", "customerAccountId": "91378e87-7a08-403b-ae47-ad46f7385ede", "customerAccountCode": "83fff3de-3bea-4340-9d92-d099ada571a0.DEFAULT", "customerMetadata": { "type": "businessCustomer", "id": "1742784", "attributes": { "createdAt": "2024-03-06T18:53:48.431Z", "name": "Brian Parsons", "address": { "street": "Street name 1", "city": "City", "state": "CA", "postalCode": "11111", "country": "US" }, "phone": { "countryCode": "1", "number": "5555555555" }, "stateOfIncorporation": "CA", "ein": "123456789", "entityType": "Corporation", "contact": { "fullName": { "first": "Brian", "last": "Parsons" }, "email": "brian@twisp.com", "phone": { "countryCode": "1", "number": "5555555555" } }, "tags": {}, "authorizedUsers": [], "status": "Active" }, "relationships": { "org": { "data": { "type": "org", "id": "4975" } } } } } ``` **Request** ```graphql mutation CreateDepositAccount( $customerAccountSetId: UUID! $depositAccountSetId: UUID! $depositAccountId: UUID! $depositAccountCode: String! $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $depositAccountSetName: String! = "Deposit Account Set" $depositAccountName: String! = "Deposit Default Account" $depositMetadata: JSON $description: String = "create a deposit account." ) { depositSet: createAccountSet( input: { accountSetId: $depositAccountSetId name: $depositAccountSetName description: $description metadata: $depositMetadata journalId: $journalId } ) { accountSetId } depositDefault: createAccount( input: { accountId: $depositAccountId name: $depositAccountName description: $description code: $depositAccountCode metadata: $depositMetadata } ) { accountId } depositToCustomer: addToAccountSet( id: $customerAccountSetId member: { memberType: ACCOUNT_SET, memberId: $depositAccountSetId } ) { accountSetId } defaultDepositToSet: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT, memberId: $depositAccountId } ) { accountSetId } } ``` **Response** ```json { "data": { "depositSet": { "accountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e" }, "depositDefault": { "accountId": "9963d6d2-2458-4f67-8bed-7604a5e0281e" }, "depositToCustomer": { "accountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0" }, "defaultDepositToSet": { "accountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e" } } } ``` **Variables** ```json { "customerAccountSetId": "83fff3de-3bea-4340-9d92-d099ada571a0", "depositAccountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e", "depositAccountId": "9963d6d2-2458-4f67-8bed-7604a5e0281e", "depositAccountCode": "8801ced3-e6a3-4af6-b01a-514473051d4e.DEFAULT", "depositMetadata": { "id": "2890313", "type": "depositAccount", "attributes": { "hold": 0, "name": "Brian Parsons", "tags": { "purpose": "checking" }, "status": "Open", "balance": 0, "currency": "USD", "available": 0, "createdAt": "2024-03-13T20:47:56.985Z", "updatedAt": "2024-03-13T20:47:56.985Z", "routingNumber": "812345678", "depositProduct": "checking" }, "relationships": { "org": { "data": { "id": "4975", "type": "org" } }, "bank": { "data": { "id": "1", "type": "bank" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } } } ``` **Request** ```graphql mutation CreateCard( $depositAccountSetId: UUID! $cardAccountSetId: UUID! $cardAccountId: UUID! $cardAccountCode: String! $cardAccountSetName: String! = "Card Account Set" $cardAccountName: String! = "Card Default Account" $journalId: UUID! = "00000000-0000-0000-0000-000000000000" $cardMetadata: JSON $description: String = "Create a card." ) { cardSet: createAccountSet( input: { accountSetId: $cardAccountSetId name: $cardAccountSetName description: $description metadata: $cardMetadata journalId: $journalId } ) { accountSetId } cardDefault: createAccount( input: { accountId: $cardAccountId name: $cardAccountName description: $description code: $cardAccountCode metadata: $cardMetadata } ) { accountId } cardToDeposit: addToAccountSet( id: $depositAccountSetId member: { memberType: ACCOUNT_SET, memberId: $cardAccountSetId } ) { accountSetId } defaultCardToSet: addToAccountSet( id: $cardAccountSetId member: { memberType: ACCOUNT, memberId: $cardAccountId } ) { accountSetId } } ``` **Response** ```json { "data": { "cardSet": { "accountSetId": "151d8647-09af-4d5a-ac32-39fcd55d2454" }, "cardDefault": { "accountId": "f6318c3e-4996-4d88-ba7a-0f66cb04302b" }, "cardToDeposit": { "accountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e" }, "defaultCardToSet": { "accountSetId": "151d8647-09af-4d5a-ac32-39fcd55d2454" } } } ``` **Variables** ```json { "depositAccountSetId": "8801ced3-e6a3-4af6-b01a-514473051d4e", "cardAccountSetId": "151d8647-09af-4d5a-ac32-39fcd55d2454", "cardAccountId": "f6318c3e-4996-4d88-ba7a-0f66cb04302b", "cardAccountCode": "151d8647-09af-4d5a-ac32-39fcd55d2454.DEFAULT", "cardMetadata": { "id": "1790431", "type": "businessDebitCard", "attributes": { "bin": "424242459", "tags": {}, "email": "richard@piedpiper.com", "phone": { "number": "5555555555", "countryCode": "1" }, "status": "Inactive", "address": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" }, "fullName": { "last": "Hendricks", "first": "Richard" }, "createdAt": "2024-03-13T20:56:00.442Z", "dateOfBirth": "2001-08-10", "last4Digits": "2860", "expirationDate": "2028-03", "shippingAddress": { "city": "Palo Alto", "state": "CA", "street": "5230 Newell Rd", "country": "US", "postalCode": "94303" } }, "relationships": { "account": { "data": { "id": "2890313", "type": "account" } }, "customer": { "data": { "id": "1742784", "type": "customer" } } } } } ``` Regardless of the mechanics of how/when new accounts are added, we suggest association of your own internal and third party identifiers on Twisp entities so that you can easily look up items in the future. > **Tip:** > > Picking identifiers for Twisp and ensuring there is enough data in Twisp to look up by Unit identifiers, or your own system internal identifiers is paramount for keeping data consistent. Twisp provides a few different mechanisms to aid in this: > > - Accounts have an `externalId` field that will enforce uniqueness. > - Accounts and Account Sets have a `metadata` field that's useful for populating with metadata from external systems, especially identifiers > - Utilize [UUID v5](https://www.sohamkamani.com/uuid-versions-explained/#v5-non-random-uuids) to generate deterministic identifiers for Twisp entities that correspond to their Unit counterparts. #### Credit Accounts Credit accounts are modeled slightly differently than deposit accounts: ```mermaid graph BT SET[/Credit Balance Account Set\] PAYABLE[/Credit Payable Account Set\] DEFAULT[/Credit Default Account\] LINE[/Credit Line\] DEFAULT --> PAYABLE LINE & PAYABLE --> SET ``` In this case a second account, `Credit Line` is added. This account is where we book a single entry to define the limit of the entire credit account. The other accounts function identically with the normality of the `Credit Payable` accounts being **Debit Normal**: | Entry ID | Account | Amount | Direction | Credit Balance | Payable Balance | |----------|---------|--------|-----------|----------------|-----------------| | 1 | Line | $1000 | Credit | $1000 | $0 | | 2 | Default | $500 | Debit | $500 | $500 | | 3 | Default | $250 | Credit | $750 | $250 | | 4 | Default | $10 | Debit | $740 | $260 | This gives you two significant balances: 1. The `Credit Balance` set gives you a balance that you can use for authorization decisions. 2. The `Credit Payable` set gives you a balance that is owed to you by the customer. **Request** ```graphql mutation CreateCreditAccount( $balanceSetId: UUID! $payableSetId: UUID! $defaultAccountId: UUID! $lineAccountId: UUID! $defaultCode: String! $lineCode: String! $metadata: JSON $journalId: UUID! = "00000000-0000-0000-0000-000000000000" ) { balanceSet: createAccountSet( input: { accountSetId: $balanceSetId name: "Credit Balance" description: "Credit balance roll up for this credit account." metadata: $metadata journalId: $journalId } ) { accountSetId } payableSet: createAccountSet( input: { accountSetId: $payableSetId name: "Payable Balance" description: "Outstanding balance owed by account holder." metadata: $metadata journalId: $journalId } ) { accountSetId } lineAcct: createAccount( input: { accountId: $lineAccountId name: "Credit Line" description: "Apply credit limit to this account." code: $lineCode metadata: $metadata } ) { accountId } defaultAccount: createAccount( input: { accountId: $defaultAccountId name: "Default" description: "Default credit account" code: $defaultCode metadata: $metadata } ) { accountId } } ``` **Response** ```json { "data": { "balanceSet": { "accountSetId": "3b270302-a595-4942-a166-d5e2ab8d6a19" }, "payableSet": { "accountSetId": "3d0d7d8c-19c7-4835-98ea-979077e0972c" }, "lineAcct": { "accountId": "b7ad3970-c408-4a01-83af-8f1523b407b5" }, "defaultAccount": { "accountId": "154396fb-5114-4a9b-afb2-9276dab92570" } } } ``` **Variables** ```json { "balanceSetId": "3b270302-a595-4942-a166-d5e2ab8d6a19", "payableSetId": "3d0d7d8c-19c7-4835-98ea-979077e0972c", "defaultAccountId": "154396fb-5114-4a9b-afb2-9276dab92570", "lineAccountId": "b7ad3970-c408-4a01-83af-8f1523b407b5", "defaultCode": "CREDIT.3b270302-a595-4942-a166-d5e2ab8d6a19.DEFAULT", "lineCode": "LINE.3b270302-a595-4942-a166-d5e2ab8d6a19.DEFAULT", "metadata": {} } ``` ### Platform Accounts When operating a fintech, your organization will partner with a bank which will configure one or more actual bank accounts in order to support your operations. These include, but are not limited to: | Account | Description | |-------------------------|-----------------------------------------------------------------------------------------| | Suspense Account | An account to post transactions with unknown accounts to. | | ACH Settlement | Settlement account for ACHs. | | Bill Pay Settlement | Settlement account for Bill Pay. | | Foreign Checks Account | Settlement account for foriegn checks. | | Courtesy Credit Account | Operational accounts for crediting accounts via customer service interactions. | | Charge Off Account | Operational account to write off closing balances on uncollectable accounts. | | Fraud Losses | Operational account to write off losses for fraud. | | Levies & Garnishments | Account to collect levies and garnishments of funds. | | Cashiers Check | Settlement account for cashiers checks. | | Card Disputes | Reserve account for handling card related disputes. | | ACH Disputes | Reserve account for handling ACH related disputes. | | Interchange Revenue | Revenue account for shared interchange. | | Collected Fee Revenue | Revenue account for fees collected from accounts. | | FBO | "For Benefit Of", omnibus accounts for specific purpose. For example, virtual wallets. | | Cash | An asset account that represents all funds created in the system. | These accounts are often the "other side" of your double entry accounting and you could have multiple of these accounts across your various banking partners. For the purpose of this document, we'll consider a system with: - Cash account for assets - Card Settlement for a single BIN - ACH Settlement account for single bank partner - Suspense Account - Disputes - Revenue > **Tip:** > > There will be a few well known identifiers for these settlement accounts that will be pervasively used through the system. > > We recommend creating a library with human readable codes that allow fast look ups to use as parameters to tran codes. **Request** ```graphql mutation PlatformAccounts { cash: createAccount( input: { accountId: "db07e5cf-6cd7-4629-a952-9613578cd8ea" code: "ASSETS.CASH" name: "Cash account" description: "Cash account representing all assets the platform." normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } cardSettlement: createAccount( input: { accountId: "fabdce1f-6eb9-4630-9472-6338ed3dc6a2" code: "SETTLEMENT.VISA" name: "Visa Settlement Account" description: "Settlement account to use for Visa cards on bin 41200." config: { enableConcurrentPosting: true } } ) { accountId } achSettlement: createAccount( input: { accountId: "7c923aed-a5fc-4ded-b2f0-57db45a8b545" code: "SETTLEMENT.ACH" name: "ACH Settlement Account" description: "Settlement account to use for Bank Partner 1." config: { enableConcurrentPosting: true } } ) { accountId } suspenseAccount: createAccount( input: { accountId: "6e341e23-e35f-488c-b947-b7b8839f4c00" code: "SUSPENSE" name: "Suspense Account" description: "Account to post to when an account doesnt exist or is in a non-postable state." config: { enableConcurrentPosting: true } } ) { accountId } disputes: createAccount( input: { accountId: "08f304d5-6a63-40b9-a182-bfb732994b15" code: "DISPUTES" name: "Disputes Account" description: "Reserve account for dispute resolution." config: { enableConcurrentPosting: true } } ) { accountId } revenueAccount: createAccount( input: { accountId: "4d79af07-acd9-4a44-8291-78a573ced41d" code: "REVENUE" name: "Revenue Account" description: "Interchange and other fees collected from customer accounts." config: { enableConcurrentPosting: true } } ) { accountId } } ``` **Response** ```json { "data": { "cash": { "accountId": "db07e5cf-6cd7-4629-a952-9613578cd8ea" }, "cardSettlement": { "accountId": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2" }, "achSettlement": { "accountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545" }, "suspenseAccount": { "accountId": "6e341e23-e35f-488c-b947-b7b8839f4c00" }, "disputes": { "accountId": "08f304d5-6a63-40b9-a182-bfb732994b15" }, "revenueAccount": { "accountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } } } ``` ## Transaction Workflows When processing transactions, to end users they feel like a singular event. I swipe my card the purchase is made. However that singular event is often a series of transactions, depending on how the swipe was made (Dual vs Single message auth), or if the merchant uses multiple clearings. Card processing can be challenging as the underlying protocol allows for a wide variety of behaviors. The same is true with many other types of transactions, such as ACH's or checks. The logically singular event may have a lifecycle that encompasses many transactions. These lifecycles we call transaction workflows and here we present a simplified model that you can adapt to your particular card and ach providers. ### Primitives There are a few different primitives built into Twisp for posting transactions that will help us model these the workflows: - [Tran Codes](/docs/accounting-core/encoded-transactions) define entries are posted for a particular transaction type. - [postTransaction](/docs/reference/graphql/mutations#post-transaction) allows you to post a transaction with a specific tran code. - [voidTransaction](/docs/reference/graphql/mutations#void-transaction) posts the entries required to reverse any transaction. - [workflows](/docs/reference/graphql/mutations#workflows) allow composition of multiple transaction operations to fulfill specific use cases. Each of these transaction flows will be composed together of one or more of the above primitives. These workflows are not prescriptive, but are intended to illustrate the concepts to the point where they can be adapted to your own use case and production usage. ## Card Authorizations and Transactions The ISO-8583 specification defines how merchants and issuers interact with each other via card networks. This communication protocol is implemented by a wide variety of vendors, and for the purposes of this document we're going to explore the key workflows required, define the [tran codes](/docs/accounting-core/encoded-transactions) required to post transaction and illustrate how to utilize those tran codes to fulfill card authorization work flows. ### Tran Codes At the heart of processing transactions in Twisp are the Tran Codes required to make journal entries. Here is a set of tran codes that allow you to completely model card authorization and settlement/clearing. | Code | Description | |-------------------|-------------------------------------------------------------| | CARD_HOLD | Post at pending layer between settlement and card accounts. | | CARD_SETTLE | Post at settled layer between settlement and card accounts. | | CARD_HOLD_VOID | Optional $0 entries at pending layer | | CARD_HOLD_REPLACE | Identical to CARD_HOLD, but provides labeling differences | | CARD_DECLINE | Optional $0 entries at pending layer for declines | **Request** ```graphql mutation CardTranCodes( $cardHold: TranCodeInput! $cardHoldVoid: TranCodeInput! $cardHoldReplace: TranCodeInput! $cardSettle: TranCodeInput! $cardDecline: TranCodeInput! ) { cardHold: createTranCode(input: $cardHold) { ...TC } cardHoldVoid: createTranCode(input: $cardHoldVoid) { ...TC } cardHoldReplace: createTranCode(input: $cardHoldReplace) { ...TC } cardSettle: createTranCode(input: $cardSettle) { ...TC } cardDecline: createTranCode(input: $cardDecline) { ...TC } } fragment TC on TranCode { tranCodeId code description params { name type default description } transaction { effective journalId correlationId externalId description metadata } entries { entryType accountId layer direction units currency description metadata condition } } ``` **Response** ```json { "data": { "cardHold": { "tranCodeId": "9f4f3293-539d-4c81-b3b9-3c8d94fa67ba", "code": "CARD_HOLD", "description": "Place an authorization hold on an account for the amount.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldVoid": { "tranCodeId": "218f19fc-e283-4f5b-90f6-02fd99e48e5c", "code": "CARD_HOLD_VOID", "description": "Posts $0 entries to an account indicating a card hold was voided. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "'Correlation identifier to group related transactions.'" }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_VOID_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_VOID_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldReplace": { "tranCodeId": "b0b6cb20-7c9d-4e48-8179-caa9c06e44d7", "code": "CARD_HOLD_REPLACE", "description": "Posts a hold at pending layer indicating the hold amount changed to this value. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_REPLACE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_REPLACE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardSettle": { "tranCodeId": "2e1f3ed4-0f0e-467a-82f7-715c2bcd97ef", "code": "CARD_SETTLE", "description": "Post a card settlement at the settled layer.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_SETTLE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_SETTLE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardDecline": { "tranCodeId": "2b954d71-01a8-4021-82a1-c78aa1b607bd", "code": "CARD_DECLINE", "description": "Note a card decline by posting a $0 transaction. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_DECLINE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_DECLINE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] } } } ``` **Variables** ```json { "cardHold": { "tranCodeId": "9f4f3293-539d-4c81-b3b9-3c8d94fa67ba", "code": "CARD_HOLD", "description": "Place an authorization hold on an account for the amount.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldVoid": { "tranCodeId": "218f19fc-e283-4f5b-90f6-02fd99e48e5c", "code": "CARD_HOLD_VOID", "description": "Posts $0 entries to an account indicating a card hold was voided. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "'Correlation identifier to group related transactions.'" }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_VOID_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_VOID_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardHoldReplace": { "tranCodeId": "b0b6cb20-7c9d-4e48-8179-caa9c06e44d7", "code": "CARD_HOLD_REPLACE", "description": "Posts a hold at pending layer indicating the hold amount changed to this value. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_HOLD_REPLACE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_HOLD_REPLACE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardSettle": { "tranCodeId": "2e1f3ed4-0f0e-467a-82f7-715c2bcd97ef", "code": "CARD_SETTLE", "description": "Post a card settlement at the settled layer.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_SETTLE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_SETTLE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] }, "cardDecline": { "tranCodeId": "2b954d71-01a8-4021-82a1-c78aa1b607bd", "code": "CARD_DECLINE", "description": "Note a card decline by posting a $0 transaction. Use in conjuction with voidTransaction.", "params": [ { "name": "account", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccount", "type": "UUID", "default": "fabdce1f-6eb9-4630-9472-6338ed3dc6a2", "description": "The settlement account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The decimal amount of the hold." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the hold amount." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of hold. Usually DEBIT." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CARD_DECLINE_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.account)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed on account in parameters.'", "metadata": "{}", "condition": null }, { "entryType": "'CARD_DECLINE_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "PENDING", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "decimal('0')", "currency": "params.currency", "description": "'Card hold placed in settlement account.'", "metadata": "{}", "condition": null } ] } } ``` ### Card Authorization Workflows Card flows in Twisp can be modeled entirely with `postTransaction` and `voidTransaction` which are the most common primitives you'll use in Twisp. Together with the set of card tran codes, you can build out very simple card flows that allow you to accurately track the balances and entries required for a card transaction lifecycle. Using the account we onboarded earlier, we'll post the transactions for each use case and print the resulting balances. #### Authorization Approval You receive an authorization request and/or an advice for $10 that an authorization was approved. | Event | Operation | Tran Code | Description | |--------------|-----------------|-----------|--------------------------------| | auth.request | postTransaction | CARD_HOLD | post $ amount to pending layer | | auth.created | voidTransaction | n/a | void previous transaction | | ... | postTransaction | CARD_HOLD | post $ amount to pending layer | **Request** ```graphql mutation CardAuthorizationApproval( $initialAuthorizationId: UUID! $authorizationId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "81832b7a-8881-48fe-b309-3bff1fcfc986" }, "initialAuthorization": { "transactionId": "2f05a4d4-07c5-42c1-a567-438737b7a0ab" }, "voidInitialAuthorization": { "transactionId": "988c1dce-ad47-52e3-8d37-4d79b60c8e66" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "initialAuthorizationId": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "authorizationId": "81832b7a-8881-48fe-b309-3bff1fcfc986", "correlation": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "effective": "2022-10-01", "initAuthHook": "{\"status\":\"AUTHORIZATION\",\"amount\":1000,\"acquirer_fee\":100,\"authorization_amount\":1000,\"settled_amount\":0,\"events\":[],\"created\":\"2022-10-01T00:00:00Z\",\"token\":\"2f05a4d4-07c5-42c1-a567-438737b7a0ab\"}", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"81832b7a-8881-48fe-b309-3bff1fcfc986\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"ef867c4d-53fc-49e5-ba8c-342209eb9b5c\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$10.00" } } } } } ``` #### Authorization Decline You decide to decline an authorization request or receive an advice that an authorization was declined for $10. | Event | Operation | Tran Code | Description | |--------------|-----------------|----------------|---------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | ... | check balance | n/a | balance exceeds threshold | | ... | voidTransaction | n/a | void transaction | | ... | postTransaction | CARD_HOLD_VOID | optionally Post $0 entry indicating no hold | **Request** ```graphql mutation CardAuthorizationDecline( $initialAuthorizationId: UUID! $declineId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $declineHook: JSON ) { # Received initial authorization webhook # and return resulting balances. initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId entries(first: 4) { nodes { balance { available(layer: PENDING) { normalBalance { units } } } } } } # Declined transaction, later recieved declined webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } decline: postTransaction( input: { transactionId: $declineId tranCode: "CARD_DECLINE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $declineHook } } ) { transactionId } } ``` **Response** ```json { "data": { "decline": { "transactionId": "3318a65f-2245-4058-bd6c-9bee76f8b0af" }, "initialAuthorization": { "entries": { "nodes": [ { "balance": { "available": { "normalBalance": { "units": "-20.00" } } } }, { "balance": { "available": { "normalBalance": { "units": "10.00" } } } } ] }, "transactionId": "67734487-03fc-4451-b9f3-c89fce63ab8f" }, "voidInitialAuthorization": { "transactionId": "de376aa4-0163-5294-a9d2-d44389fb24b5" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "initialAuthorizationId": "67734487-03fc-4451-b9f3-c89fce63ab8f", "declineId": "3318a65f-2245-4058-bd6c-9bee76f8b0af", "correlation": "67734487-03fc-4451-b9f3-c89fce63ab8f", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"67734487-03fc-4451-b9f3-c89fce63ab8f\"\n }", "declineHook": "{\n \"status\": \"DECLINED\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"UNAUTHORIZED_MERCHANT\",\n \"token\": \"3318a65f-2245-4058-bd6c-9bee76f8b0af\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"67734487-03fc-4451-b9f3-c89fce63ab8f\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$10.00" } } } } } ``` #### Authorization Update An authorization is approved for $10 and then updated to $1. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------------|------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | auth.update | voidTransaction | n/a | void prior transaction | | ... | postTransaction | CARD_HOLD_REPLACE | post new $ amount to pending layer | **Request** ```graphql mutation CardAuthorizationUpdate( $initialAuthorizationId: UUID! $authorizationId: UUID! $updateId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $updateAmount: Decimal $effective: Date $initAuthHook: JSON $authHook: JSON $updateHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Received update webhook voidApprovedAuth: voidTransaction(id: $authorizationId) { transactionId } update: postTransaction( input: { transactionId: $updateId tranCode: "CARD_HOLD_REPLACE" params: { account: $accountId amount: $updateAmount correlation: $correlation effective: $effective metadata: $updateHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "79c336e6-8782-4df5-9992-c7c9bab0ad4e" }, "initialAuthorization": { "transactionId": "449afa1d-95dd-406f-9d72-c8b4396055f9" }, "update": { "transactionId": "c97c23d5-02c1-4361-b371-f1e40c50a73f" }, "voidApprovedAuth": { "transactionId": "48494e7e-553d-540e-a1aa-a2f3533c29b3" }, "voidInitialAuthorization": { "transactionId": "7d672168-0883-5f58-9fb7-6f1f89e42e48" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "updateAmount": "1.00", "updateId": "c97c23d5-02c1-4361-b371-f1e40c50a73f", "initialAuthorizationId": "449afa1d-95dd-406f-9d72-c8b4396055f9", "authorizationId": "79c336e6-8782-4df5-9992-c7c9bab0ad4e", "correlation": "449afa1d-95dd-406f-9d72-c8b4396055f9", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"449afa1d-95dd-406f-9d72-c8b4396055f9\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"79c336e6-8782-4df5-9992-c7c9bab0ad4e\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"449afa1d-95dd-406f-9d72-c8b4396055f9\"\n }", "updateHook": "{\n \"status\": \"PENDING\",\n \"amount\": 100,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 100,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"79c336e6-8782-4df5-9992-c7c9bab0ad4e\"\n },\n {\n \"amount\": -900,\n \"type\": \"VOID\",\n \"result\": \"APPROVED\",\n \"token\": \"c97c23d5-02c1-4361-b371-f1e40c50a73f\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"449afa1d-95dd-406f-9d72-c8b4396055f9\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$11.00" } } } } } ``` #### Authorization Cancelation The authorization is created for $10 that expires or is canceled by the merchant. | Event | Operation | Tran Code | Description | |--------------|-----------------|--------------|-----------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | auth.cancel | voidTransaction | n/a | void prior transaction | **Request** ```graphql mutation CardAuthorizationCancel( $initialAuthorizationId: UUID! $authorizationId: UUID! $expireId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON $expireHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Recieved card expiry webhook voidAuthorization: voidTransaction(id: $authorizationId) { transactionId } expire: postTransaction( input: { transactionId: $expireId tranCode: "CARD_HOLD_VOID" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $expireHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "b73a6058-d423-4543-9aba-0ff54c4e0c27" }, "expire": { "transactionId": "db083442-d2a3-45a9-abe5-24023fdb10fb" }, "initialAuthorization": { "transactionId": "a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c" }, "voidAuthorization": { "transactionId": "9fe6671e-aba5-5b4c-a986-739271521c8a" }, "voidInitialAuthorization": { "transactionId": "14984ed2-b2b6-551f-b9c8-32b32c1f2590" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "initialAuthorizationId": "a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c", "authorizationId": "b73a6058-d423-4543-9aba-0ff54c4e0c27", "expireId": "db083442-d2a3-45a9-abe5-24023fdb10fb", "correlation": "a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"b73a6058-d423-4543-9aba-0ff54c4e0c27\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c\"\n }", "expireHook": "{\n \"status\": \"VOIDED\",\n \"amount\": 0,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 0,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"b73a6058-d423-4543-9aba-0ff54c4e0c27\"\n },\n {\n \"amount\": -1000,\n \"type\": \"AUTHORIZATION_EXPIRY\",\n \"result\": \"APPROVED\",\n \"token\": \"db083442-d2a3-45a9-abe5-24023fdb10fb\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"a9551772-ca3d-4dc2-9da2-0d74c1ac1c7c\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "$0.00" } }, "pending": { "normalBalance": { "formatted": "-$11.00" } } } } } ``` #### Settlement (amount >= auth) A settlement of an existing authorization whose amount is greater than the hold amount. In this case a $10 settlement is applied to the account. | Event | Operation | Tran Code | Description | |--------------|-----------------|----------------|---------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | | ... | voidTransaction | n/a | void prior authorization transaction | | ... | postTransaction | CARD_HOLD_VOID | optionally Post $0 entry indicating no hold | **Request** ```graphql mutation CardSettlement( $initialAuthorizationId: UUID! $authorizationId: UUID! $settleId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON $settleHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Recieved clearing webhook voidAuthorization: voidTransaction(id: $authorizationId) { transactionId } clearing: postTransaction( input: { transactionId: $settleId tranCode: "CARD_SETTLE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "5e20e176-82bd-49b4-955c-c059b153b5db" }, "clearing": { "transactionId": "108449c6-122c-43a4-936c-228056e8ee58" }, "initialAuthorization": { "transactionId": "3b8937a2-bf03-4bd4-83e8-ededf56118e9" }, "voidAuthorization": { "transactionId": "19c18c18-5a6d-505c-846b-08debddde872" }, "voidInitialAuthorization": { "transactionId": "6a27101a-b1c4-5f54-a510-59fcf5667fe2" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "settleId": "108449c6-122c-43a4-936c-228056e8ee58", "initialAuthorizationId": "3b8937a2-bf03-4bd4-83e8-ededf56118e9", "authorizationId": "5e20e176-82bd-49b4-955c-c059b153b5db", "correlation": "3b8937a2-bf03-4bd4-83e8-ededf56118e9", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3b8937a2-bf03-4bd4-83e8-ededf56118e9\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"5e20e176-82bd-49b4-955c-c059b153b5db\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3b8937a2-bf03-4bd4-83e8-ededf56118e9\"\n }", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": 1000,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 0,\n \"settled_amount\": 1000,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"5e20e176-82bd-49b4-955c-c059b153b5db\"\n },\n {\n \"amount\": 1000,\n \"type\": \"CLEARING\",\n \"result\": \"APPROVED\",\n \"token\": \"108449c6-122c-43a4-936c-228056e8ee58\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3b8937a2-bf03-4bd4-83e8-ededf56118e9\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$10.00" } }, "pending": { "normalBalance": { "formatted": "-$21.00" } } } } } ``` #### Multi-settlement (amount < auth) Some processors always drop holds with a settlement. Others will adjust hold amount and allow for additional settlements. You'll map your implementation to match the card providers. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------------|---------------------------------------------| | auth.created | postTransaction | CARD_HOLD | post $ amount to pending layer | | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | | ... | voidTransaction | n/a | void prior authorization transaction | | ... | postTransaction | CARD_HOLD_VOID | optionally Post $0 entry indicating no hold | | auth.updated | voidTransaction | n/a | optionally void prior $0 hold | | ... | postTransaction | CARD_HOLD_REPLACE | optionally post new hold | **Request** ```graphql mutation CardPartialSettlement( $initialAuthorizationId: UUID! $authorizationId: UUID! $partialSettleId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $replaceAmount: Decimal! $settleAmount: Decimal! $effective: Date $initAuthHook: JSON $authHook: JSON $settleHook: JSON ) { # Received initial authorization webhook initialAuthorization: postTransaction( input: { transactionId: $initialAuthorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $initAuthHook } } ) { transactionId } # Recieved approved webhook voidInitialAuthorization: voidTransaction(id: $initialAuthorizationId) { transactionId } authorization: postTransaction( input: { transactionId: $authorizationId tranCode: "CARD_HOLD" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $authHook } } ) { transactionId } # Recieved partial settlement webhook voidAuthorization: voidTransaction(id: $authorizationId) { transactionId } updateHold: postTransaction( input: { transactionId: $partialSettleId tranCode: "CARD_HOLD_REPLACE" params: { account: $accountId amount: $replaceAmount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } partialSettle: postTransaction( input: { transactionId: "6daad172-6ae7-4ddd-843c-e5e0455fb6da" tranCode: "CARD_SETTLE" params: { account: $accountId amount: $settleAmount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } } ``` **Response** ```json { "data": { "authorization": { "transactionId": "9588d5a5-26a2-4ed0-84bb-384f4097c7f5" }, "initialAuthorization": { "transactionId": "3175506b-d828-4deb-ae6b-5aabeebd438d" }, "partialSettle": { "transactionId": "6daad172-6ae7-4ddd-843c-e5e0455fb6da" }, "updateHold": { "transactionId": "3c9faea9-1807-4867-967a-0fadd0b18521" }, "voidAuthorization": { "transactionId": "bcbf86b3-101c-5605-b872-6587d381c477" }, "voidInitialAuthorization": { "transactionId": "4f87f582-d440-558b-801d-4176f0056cc8" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "replaceAmount": "9.00", "settleAmount": "1.00", "initialAuthorizationId": "3175506b-d828-4deb-ae6b-5aabeebd438d", "authorizationId": "9588d5a5-26a2-4ed0-84bb-384f4097c7f5", "partialSettleId": "3c9faea9-1807-4867-967a-0fadd0b18521", "correlation": "3175506b-d828-4deb-ae6b-5aabeebd438d", "effective": "2022-10-01", "initAuthHook": "{\n \"status\": \"AUTHORIZATION\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3175506b-d828-4deb-ae6b-5aabeebd438d\"\n }", "authHook": "{\n \"status\": \"PENDING\",\n \"amount\": 1000,\n \"acquirer_fee\": 100,\n \"authorization_amount\": 1000,\n \"settled_amount\": 0,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"9588d5a5-26a2-4ed0-84bb-384f4097c7f5\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3175506b-d828-4deb-ae6b-5aabeebd438d\"\n }", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": 100,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 900,\n \"settled_amount\": 100,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"AUTHORIZATION\",\n \"result\": \"APPROVED\",\n \"token\": \"9588d5a5-26a2-4ed0-84bb-384f4097c7f5\"\n },\n {\n \"amount\": 100,\n \"type\": \"CLEARING\",\n \"result\": \"APPROVED\",\n \"token\": \"3c9faea9-1807-4867-967a-0fadd0b18521\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3175506b-d828-4deb-ae6b-5aabeebd438d\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$11.00" } }, "pending": { "normalBalance": { "formatted": "-$31.00" } } } } } ``` #### Settlement (no Auth) Receive a transaction without a matching hold. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------|---------------------------------------------| | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | **Request** ```graphql mutation CardForcePost( $settlementId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $effective: Date $settleHook: JSON ) { # Received clearing webhook. forcePost: postTransaction( input: { transactionId: $settlementId tranCode: "CARD_SETTLE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $settleHook } } ) { transactionId } } ``` **Response** ```json { "data": { "forcePost": { "transactionId": "d0150140-0ef1-4b89-8c23-7ad537006329" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "settlementId": "d0150140-0ef1-4b89-8c23-7ad537006329", "correlation": "3a47f3fc-de3b-44b9-aea2-74e0c02e1fb8", "effective": "2022-10-01", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": 1000,\n \"acquirer_fee\": 0,\n \"authorization_amount\": 1000,\n \"settled_amount\": 1000,\n \"events\": [\n {\n \"amount\": 1000,\n \"type\": \"CLEARING\",\n \"result\": \"APPROVED\",\n \"token\": \"d0150140-0ef1-4b89-8c23-7ad537006329\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"3a47f3fc-de3b-44b9-aea2-74e0c02e1fb8\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$21.00" } }, "pending": { "normalBalance": { "formatted": "-$41.00" } } } } } ``` #### Return Receive a return transaction. | Event | Operation | Tran Code | Description | |--------------|-----------------|-------------|---------------------------------------------| | tx.created | postTransaction | CARD_SETTLE | post $ amount to settled layer | **Request** ```graphql mutation CardReturn( $settlementId: UUID! $accountId: UUID! $correlation: String $direction: String $amount: Decimal! $effective: Date $settleHook: JSON ) { # Received clearing webhook. forcePost: postTransaction( input: { transactionId: $settlementId tranCode: "CARD_SETTLE" params: { account: $accountId amount: $amount correlation: $correlation effective: $effective metadata: $settleHook direction: $direction } } ) { transactionId } } ``` **Response** ```json { "data": { "forcePost": { "transactionId": "fa5cb793-0739-4eb8-8db2-9b452ed71930" } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "direction": "CREDIT", "settlementId": "fa5cb793-0739-4eb8-8db2-9b452ed71930", "correlation": "265a9120-e42a-4c02-b3dc-eb80a2e3cf00", "effective": "2022-10-01", "settleHook": "{\n \"status\": \"SETTLED\",\n \"amount\": -1000,\n \"acquirer_fee\": 0,\n \"authorization_amount\": -1000,\n \"settled_amount\": -1000,\n \"events\": [\n {\n \"amount\": -1000,\n \"type\": \"RETURN\",\n \"result\": \"APPROVED\",\n \"token\": \"fa5cb793-0739-4eb8-8db2-9b452ed71930\"\n }\n ],\n \"created\": \"2022-10-01T00:00:00Z\",\n \"token\": \"265a9120-e42a-4c02-b3dc-eb80a2e3cf00\"\n }" } ``` **Request** ```graphql query CheckBalance($accountId: UUID! = "7a17b0f1-b04f-46a4-ba45-d52a861c21d9") { balance(accountId: $accountId) { settled: available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } pending: available(layer: PENDING) { normalBalance { formatted(as: { locale: "en-US" }) } } } } ``` **Response** ```json { "data": { "balance": { "settled": { "normalBalance": { "formatted": "-$11.00" } }, "pending": { "normalBalance": { "formatted": "-$31.00" } } } } } ``` ## ACH The Automated Clearing House is a service provided by the Federal Reserve for conducting electronic funds transfers. There are two modes of operation when dealing with ACH: - Receiving Deposit Financial Institution (RDFI): External FI's are crediting/debiting your accounts with theirs - Originating Deposit Financial Institution (ODFI): You are initiating credits/debits with external accounts Twisp has a number of built in tran codes and workflows to handle both use cases. ### Tran Codes **Request** ```graphql query ACHTranCodes { achEncumbranceCancelDebit: tranCode( id: "7b24410d-6dbc-44cd-8779-a1bfa827868a" ) { ...TC } achEncumbranceCancelReversalCredit: tranCode( id: "b7976c0e-2e4c-4e99-a48f-86384f3dddf6" ) { ...TC } achEncumbranceCredit: tranCode(id: "8fe41dca-190a-4c84-adc9-78952ac70a54") { ...TC } achEncumbranceDebit: tranCode(id: "0b83d0d4-b580-4056-a094-74ef393328b7") { ...TC } achEncumbranceReturnDebit: tranCode( id: "cde02e54-e725-46bc-9b61-5511c93058a7" ) { ...TC } achEncumbranceReturnCredit: tranCode( id: "4c4d9599-1caa-4632-a6fb-ca1ae3a2b836" ) { ...TC } achEncumbranceReversalDebit: tranCode( id: "cd7b997c-526c-4b8f-ac56-8c6de009dc25" ) { ...TC } achEncumbranceReversalCredit: tranCode( id: "f57ba3a2-ccfa-47de-ae81-1a8167639fe3" ) { ...TC } achFeeDebit: tranCode(id: "d3b64ee6-0815-4d06-aa9b-906bc48cbc19") { ...TC } achFeeReimburseCredit: tranCode(id: "49b0f890-75e7-4b94-920b-54328c02b2ca") { ...TC } achPendingDebit: tranCode(id: "e19dfe3f-6513-425a-b9a3-870f91303cc8") { ...TC } achPendingCancelCredit: tranCode(id: "c66725d7-c115-4339-89e9-69b6a6ae97bd") { ...TC } achPendingCancelReversalDebit: tranCode( id: "f83a1768-06a1-4a6b-a472-6e9b9c7dbe75" ) { ...TC } achPendingReversalDebit: tranCode( id: "21036f96-bc05-4526-8004-e63ff6955d0a" ) { ...TC } achSettleCredit: tranCode(id: "d918eb34-1ef9-4437-a82b-46c169f63e41") { ...TC } achSettleDebit: tranCode(id: "5af51352-84ad-4534-aacf-4e68c2ed2e2b") { ...TC } achSettleReturnCredit: tranCode(id: "90e09ed8-ae9c-46f2-9be8-2e09b4a5de35") { ...TC } achSettleReturnDebit: tranCode(id: "2b11b428-54ed-429c-97d9-b9a4e3ef6007") { ...TC } } fragment TC on TranCode { tranCodeId code description params { name type default description } transaction { effective journalId correlationId externalId description metadata } entries { entryType accountId layer direction units currency description metadata condition } } ``` **Response** ```json { "data": { "achEncumbranceCancelDebit": { "tranCodeId": "7b24410d-6dbc-44cd-8779-a1bfa827868a", "code": "SYS_ACH_ENCUMBRANCE_CANCEL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CANCEL_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_CANCEL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceCancelReversalCredit": { "tranCodeId": "b7976c0e-2e4c-4e99-a48f-86384f3dddf6", "code": "SYS_ACH_ENCUMBRANCE_CANCEL_REVERSAL_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CANCEL_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_CANCEL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceCredit": { "tranCodeId": "8fe41dca-190a-4c84-adc9-78952ac70a54", "code": "SYS_ACH_ENCUMBRANCE_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceDebit": { "tranCodeId": "0b83d0d4-b580-4056-a094-74ef393328b7", "code": "SYS_ACH_ENCUMBRANCE_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReturnDebit": { "tranCodeId": "cde02e54-e725-46bc-9b61-5511c93058a7", "code": "SYS_ACH_ENCUMBRANCE_RETURN_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_RETURN_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_RETURN_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReturnCredit": { "tranCodeId": "4c4d9599-1caa-4632-a6fb-ca1ae3a2b836", "code": "SYS_ACH_ENCUMBRANCE_RETURN_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_RETURN_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_RETURN_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReversalDebit": { "tranCodeId": "cd7b997c-526c-4b8f-ac56-8c6de009dc25", "code": "SYS_ACH_ENCUMBRANCE_REVERSAL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achEncumbranceReversalCredit": { "tranCodeId": "f57ba3a2-ccfa-47de-ae81-1a8167639fe3", "code": "SYS_ACH_ENCUMBRANCE_REVERSAL_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achFeeDebit": { "tranCodeId": "d3b64ee6-0815-4d06-aa9b-906bc48cbc19", "code": "SYS_ACH_FEE_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_FEE_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_FEE_CR'", "accountId": "uuid(params.feeAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achFeeReimburseCredit": { "tranCodeId": "49b0f890-75e7-4b94-920b-54328c02b2ca", "code": "SYS_ACH_FEE_REIMBURSE_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_FEE_REIMBURSE_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_FEE_REIMBURSE_DR'", "accountId": "uuid(params.feeAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingDebit": { "tranCodeId": "e19dfe3f-6513-425a-b9a3-870f91303cc8", "code": "SYS_ACH_PENDING_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingCancelCredit": { "tranCodeId": "c66725d7-c115-4339-89e9-69b6a6ae97bd", "code": "SYS_ACH_PENDING_CANCEL_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_CANCEL_CR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_CANCEL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingCancelReversalDebit": { "tranCodeId": "f83a1768-06a1-4a6b-a472-6e9b9c7dbe75", "code": "SYS_ACH_PENDING_CANCEL_REVERSAL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_CANCEL_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_CANCEL_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achPendingReversalDebit": { "tranCodeId": "21036f96-bc05-4526-8004-e63ff6955d0a", "code": "SYS_ACH_PENDING_REVERSAL_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_PENDING_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleCredit": { "tranCodeId": "d918eb34-1ef9-4437-a82b-46c169f63e41", "code": "SYS_ACH_SETTLE_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleDebit": { "tranCodeId": "5af51352-84ad-4534-aacf-4e68c2ed2e2b", "code": "SYS_ACH_SETTLE_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleReturnCredit": { "tranCodeId": "90e09ed8-ae9c-46f2-9be8-2e09b4a5de35", "code": "SYS_ACH_SETTLE_RETURN_CR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_RETURN_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_RETURN_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] }, "achSettleReturnDebit": { "tranCodeId": "2b11b428-54ed-429c-97d9-b9a4e3ef6007", "code": "SYS_ACH_SETTLE_RETURN_DR", "description": "", "params": [ { "name": "accountId", "type": "UUID", "default": null, "description": "The account to place the hold on." }, { "name": "settlementAccountId", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "feeAccountId", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "Optional fee account to use." }, { "name": "feeAmount", "type": "DECIMAL", "default": "0", "description": "Optional decimal amount of the fee." }, { "name": "journalId", "type": "UUID", "default": null, "description": "The journal to post transactions to." }, { "name": "amount", "type": "DECIMAL", "default": null, "description": "The decimal amount." }, { "name": "correlationId", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "metadata for the transaction." }, { "name": "entryMetadata", "type": "JSON", "default": "{}", "description": "metadata for the entry of accountId." } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_RETURN_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null }, { "entryType": "'ACH_SETTLE_RETURN_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "params.entryMetadata", "condition": null } ] } } } ``` ### Workflows In addition to the built-in tran codes, Twisp offers a number of workflows that will post & void transactions as appropriate for each stage of the ACH transaction lifecycle. These are built on top of the [executeTask](/docs/reference/graphql/mutations#workflow.execute-task) workflow invocation. Below we will look at sample graphql for invocation of the workflow and describe the various tasks available for each type of workflow. #### ODFI Push | Task | Valid From | Description | |---------------|------------------------|---------------------------------------------------------------------------------------------| | CREATE | | Debit customers account at `PENDING` layer, funds leaving in next ACH batch. | | SUBMIT | CREATE | Void the `PENDING` transaction and post settlement, funds are sent to external institution. | | RETURN | SUBMIT | External institution returned the funds for some reason. e.g. Account is closed. | | CANCEL | CREATE | Cancel the ACH transaction before the batch window is reached. | | CONTINUE | CANCEL, REIMBURSE_FEE | Undo the cancellation. | | REIMBURSE_FEE | CREATE, SUBMIT, RETURN | Reimburse optional fee to the customer. | **Request** ```graphql mutation ODFIPush( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! $feeAccountId: UUID! ) { # Push is scheduled create: workflow { executeTask( input: { task: "CREATE" workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" params: { accountId: $accountId feeAccountId: $feeAccountId # optional fee accountId for fees settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" # optional fee amount feeAmount: "0.50" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Fee can be reimbursed after CREATE # reimburse: workflow { # executeTask( # input: { # task: "REIMBURSE_FEE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # cancel will reimburse fee if not reimbursed yet # cancel: workflow { # executeTask( # input: { # task: "CANCEL" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # continue allows workflow to progress to submit # the fee will still apply depending on whether a REIMBURSE_FEE occurred # continue: workflow { # executeTask( # input: { # task: "CONTINUE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # submit means in an ach file on the way to Fed. submit: workflow { executeTask( input: { task: "SUBMIT" workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # fee can be reimbursed after SUBMIT # reimburse: workflow { # executeTask( # input: { # task: "REIMBURSE_FEE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # The external institution returned the ACH. return: workflow { executeTask( input: { task: "RETURN" workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # fee can be reimbursed after RETURN # reimburse: workflow { # execute( # input: { # task: "REIMBURSE_FEE" # workflowId: "934498b5-b4f1-46c4-ad79-868939dc39e8" # executionId: "34d499dd-d061-42f4-af42-c051c8d4109e" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) # } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_PENDING_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_PENDING_CR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "0.50" }, "direction": "DEBIT", "entryType": "ACH_FEE_DR" }, { "amount": { "units": "0.50" }, "direction": "CREDIT", "entryType": "ACH_FEE_CR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" } ] } } ] } }, "submit": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_PENDING_REVERSAL_DR" }, { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_PENDING_REVERSAL_CR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` #### ODFI Pull | State | Valid From | Description | |--------|----------------|------------------------------------------------------------------------------------| | CREATE | | Credit customer account from an external account at `ENCUMBRANCE` layer. | | CANCEL | CREATE | Cancel transfer if before ACH batch cutoff. | | SUBMIT | CREATE | Submit transfer in ACH file. Post at `PENDING` layer until settlement. | | SETTLE | SUBMIT | After 3 days without return, post transfer to `SETTLED` layer. | | RETURN | SUBMIT, SETTLE | If received a return, send funds from customer account back to settlement account. | **Request** ```graphql mutation ODFIPull( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! ) { # Transfer will happen in next batch. Funds credited at ENCUMBRANCE layer create: workflow { executeTask( input: { task: "CREATE" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { accountId: $accountId settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Can cancel after create # cancel: workflow { # executeTask( # input: { # task: "CANCEL" # workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" # executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # Can undo cancellation # continue: workflow { # executeTask( # input: { # task: "CONTINUE" # workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" # executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" # params: { # effective: "2023-01-01" # metadata: "{}" # } # } # ) { # transactions { # transactionId # } # } # } # Transfer is in the ACH file. Funds are credited at PENDING layer. submit: workflow { executeTask( input: { task: "SUBMIT" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Invoked after three days without return. Funds are credited at SETTLED layer. settle: workflow { executeTask( input: { task: "SETTLE" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # A return is recieved, move funds back to settlement account. return: workflow { executeTask( input: { task: "RETURN" workflowId: "064e3b76-6072-451e-aace-5a7be3704ee2" executionId: "3d4875e5-119d-47fe-9a5c-a0064ba8c5a8" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" } ] } } ] } }, "settle": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" }, { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" } ] } } ] } }, "submit": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" }, { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` #### RDFI Credit | State | Valid From | Description | |--------|----------------|-----------------------------------------------------------------| | CREATE | | Creates an encumbrance credit, funds will deposit into account. | | SETTLE | CREATE | Funds are now settled. | | RETURN | SETTLE, CREATE | Returned funds to originating institution. | **Request** ```graphql mutation RDFICredit( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! ) { # ACH is in file effective for future, put in PENDING layer create: workflow { executeTask( input: { task: "CREATE" workflowId: "44d71180-6db2-437e-9083-6ba672f41ba2" executionId: "779b773c-f7a3-41f3-905b-5934dcda8932" params: { accountId: $accountId settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Reached the effective date, put into SETTLED layer. settle: workflow { executeTask( input: { task: "SETTLE" workflowId: "44d71180-6db2-437e-9083-6ba672f41ba2" executionId: "779b773c-f7a3-41f3-905b-5934dcda8932" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } # Initiate a return of funds to the settlement account. return: workflow { executeTask( input: { task: "RETURN" workflowId: "44d71180-6db2-437e-9083-6ba672f41ba2" executionId: "779b773c-f7a3-41f3-905b-5934dcda8932" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" } ] } } ] } }, "settle": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" }, { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` #### RDFI Debit | State | Valid From | Description | |--------|----------------|---------------------------------------------------------------| | CREATE | | Creates and encumbrance debit, funds will debit from account. | | SETTLE | CREATE | Funds are now settled. | | RETURN | CREATE, SETTLE | Initiated a return, funds credit back to customer account. | **Request** ```graphql mutation RDFIDebit( $accountId: UUID! $journalId: UUID! $settlementAccountId: UUID! ) { create: workflow { executeTask( input: { task: "CREATE" workflowId: "c9085474-0b55-4bda-a279-04535d7cb8b7" executionId: "e3271c9d-cdcf-4f38-af96-a3916219f611" params: { accountId: $accountId settlementAccountId: $settlementAccountId journalId: $journalId amount: "1.00" effective: "2023-01-01" metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } settle: workflow { executeTask( input: { task: "SETTLE" workflowId: "c9085474-0b55-4bda-a279-04535d7cb8b7" executionId: "e3271c9d-cdcf-4f38-af96-a3916219f611" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } return: workflow { executeTask( input: { task: "RETURN" workflowId: "c9085474-0b55-4bda-a279-04535d7cb8b7" executionId: "e3271c9d-cdcf-4f38-af96-a3916219f611" params: { effective: "2023-01-01", metadata: "{}" } } ) { transactions { entries(first: 4) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "create": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_CR" } ] } } ] } }, "return": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_RETURN_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_RETURN_DR" } ] } } ] } }, "settle": { "executeTask": { "transactions": [ { "entries": { "nodes": [ { "amount": { "units": "-1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_DR" }, { "amount": { "units": "-1.00" }, "direction": "CREDIT", "entryType": "ACH_ENCUMBRANCE_REVERSAL_CR" } ] } }, { "entries": { "nodes": [ { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_SETTLE_DR" }, { "amount": { "units": "1.00" }, "direction": "CREDIT", "entryType": "ACH_SETTLE_CR" } ] } } ] } } } } ``` **Variables** ```json { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "journalId": "00000000-0000-0000-0000-000000000000", "settlementAccountId": "7c923aed-a5fc-4ded-b2f0-57db45a8b545", "feeAccountId": "4d79af07-acd9-4a44-8291-78a573ced41d" } ``` ## Clearing Settlements The cash account is an important one for double entry accounting. This is account is the mechanism for getting money from the outside world into and out of your system. In our case there are several accounts that may interact with the outside world and change the total cash position of the system: - Settlement Accounts - Cards - ACH - Revenue accounts - Disputes and other operational accounts These accounts will be "cleared" in bulk with the aggregate amount at the time of clearing. Let's say you have $2M in card debits on a particular day across 2 transactions. Where your starting cash balance is $100M | DR | CR | Balances | |--------------------|-----------------|-----------------------------| | $2M Card Settle | $2M Cash | Card Settle ($2M), Cash $98 | | $1M Deposit Acct 1 | $1M Card Settle | Card Settle($1M) | | $1m Deposit Acct 2 | $1M Card Settle | Card Settle $0 | This bulk clearing to get funds on/off the platform should be conducted with it's own tran codes. These settlement accounts are often backed by an account at a bank, and so the balances of these accounts should be reconciled with your representation of them in your accounting system. **Request** ```graphql mutation ClearingTranCode($clearing: TranCodeInput!) { createTranCode(input: $clearing) { ...TC } } fragment TC on TranCode { tranCodeId code description params { name type default description } transaction { effective journalId correlationId externalId description metadata } entries { entryType accountId layer direction units currency description metadata condition } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "2cc6935d-ca90-40a6-8420-b9054775d6b8", "code": "CLEARING", "description": "Clear funds between cash and settlement accounts.", "params": [ { "name": "settlementAccount", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "cashAccount", "type": "UUID", "default": "db07e5cf-6cd7-4629-a952-9613578cd8ea", "description": "The cash account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The amount to clear." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the clearance." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of clearing. DEBIT raises cash account balance. CREDIT lowers." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." }, { "name": "description", "type": "STRING", "default": "Clearing settlement.", "description": "describe this transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "params.description", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CLEARING_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.cashAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null }, { "entryType": "'CLEARING_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null } ] } } } ``` **Variables** ```json { "clearing": { "tranCodeId": "2cc6935d-ca90-40a6-8420-b9054775d6b8", "code": "CLEARING", "description": "Clear funds between cash and settlement accounts.", "params": [ { "name": "settlementAccount", "type": "UUID", "default": null, "description": "The settlement account to use." }, { "name": "cashAccount", "type": "UUID", "default": "db07e5cf-6cd7-4629-a952-9613578cd8ea", "description": "The cash account to use." }, { "name": "journal", "type": "UUID", "default": "00000000-0000-0000-0000-000000000000", "description": "The journal to post transactions to." }, { "name": "amount", "type": "STRING", "default": null, "description": "The amount to clear." }, { "name": "currency", "type": "STRING", "default": "USD", "description": "The currency of the clearance." }, { "name": "direction", "type": "STRING", "default": "DEBIT", "description": "CREDIT or DEBIT direction of clearing. DEBIT raises cash account balance. CREDIT lowers." }, { "name": "correlation", "type": "STRING", "default": null, "description": "Correlation identifier to group related transactions." }, { "name": "effective", "type": "DATE", "default": null, "description": "Effective date for the transaction." }, { "name": "metadata", "type": "JSON", "default": "{}", "description": "Effective date for the transaction." }, { "name": "description", "type": "STRING", "default": "Clearing settlement.", "description": "describe this transaction." } ], "transaction": { "effective": "params.effective", "journalId": "params.journal", "correlationId": "params.correlation", "externalId": "''", "description": "params.description", "metadata": "params.metadata" }, "entries": [ { "entryType": "'CLEARING_'+string(params.direction == 'DEBIT' ? 'DR' : 'CR')", "accountId": "uuid(params.cashAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null }, { "entryType": "'CLEARING_'+ string(params.direction == 'DEBIT' ? 'CR' : 'DR')", "accountId": "uuid(params.settlementAccount)", "layer": "SETTLED", "direction": "params.direction == 'DEBIT' ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency", "description": "params.description", "metadata": "{}", "condition": null } ] } } ``` ## Advanced Topics As we've seen, Tran Codes are a powerful abstraction to encapsulate accounting concerns inside of Twisp. We also explored one workflow that Twisp offers for ACH transactions, to automatically handle posting and voiding transactions to a customers account for each part of an ACH lifecycle. Twisp offers several other workflows that may be useful for interacting with the accounting core. In this section we'll briefly examine each one. ### Void And Post Workflow The Void and Post workflow is a powerful mechanism for modeling a transaction that may change layers and amounts over time. In essence a single transaction identifier, the `executionId` passed to the workflow, will void the prior transaction created by the workflow and create a new transaction with the tran code you've provided to the workflow. This allows for some powerful transaction modeling. Consider the earlier section on card holds. In that you may be composing together two graphql queries: - `voidTransaction` to void the "prior authorization" - `postTransaction` to post the new authorization value With the void and post workflow, we can use a single identifier for the "authorization" and update it without needing to keep track of multiple transaction ids. Consider this example which posts an authorization and then updates it. **Request** ```graphql mutation VoidAndPost( $authorizationId: UUID! $accountId: UUID! $correlation: String $amount: Decimal! $updatedAmount: Decimal! $effective: Date ) { originalAuth: workflow { executeTask( input: { workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4" executionId: $authorizationId task: "VOID_AND_POST" params: { tranCode: "CARD_HOLD" account: $accountId amount: $amount correlation: $correlation effective: $effective } } ) { transactions { transactionId entries(first: 10) { nodes { entryType direction amount { units } } } } } } updatedAuth: workflow { executeTask( input: { workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4" executionId: $authorizationId task: "VOID_AND_POST" params: { tranCode: "CARD_HOLD" account: $accountId amount: $updatedAmount correlation: $correlation effective: $effective } } ) { transactions { transactionId entries(first: 10) { nodes { entryType direction amount { units } } } } } } } ``` **Response** ```json { "data": { "originalAuth": { "executeTask": { "transactions": [ { "transactionId": "edcc3942-3c4f-5d40-8fa7-0a2cbc068631", "entries": { "nodes": [ { "entryType": "CARD_HOLD_DR", "direction": "DEBIT", "amount": { "units": "10.00" } }, { "entryType": "CARD_HOLD_CR", "direction": "CREDIT", "amount": { "units": "10.00" } } ] } } ] } }, "updatedAuth": { "executeTask": { "transactions": [ { "transactionId": "fed601a6-ce71-5760-8dbc-5ec5e392ff29", "entries": { "nodes": [ { "entryType": "CARD_HOLD_DR_VOID", "direction": "DEBIT", "amount": { "units": "-10.00" } }, { "entryType": "CARD_HOLD_CR_VOID", "direction": "CREDIT", "amount": { "units": "-10.00" } } ] } }, { "transactionId": "bdcdac07-29fc-5f5f-9e65-9b9a37241c9c", "entries": { "nodes": [ { "entryType": "CARD_HOLD_DR", "direction": "DEBIT", "amount": { "units": "15.00" } }, { "entryType": "CARD_HOLD_CR", "direction": "CREDIT", "amount": { "units": "15.00" } } ] } } ] } } } } ``` **Variables** ```json { "accountId": "7a17b0f1-b04f-46a4-ba45-d52a861c21d9", "amount": "10.00", "updatedAmount": "15.00", "authorizationId": "a60f3df6-a1e8-4c04-bac2-bc9d15e94bc3", "correlation": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "effective": "2022-10-01" } ``` ### States Language Workflow GraphQL is an extremely expressive querying language, however sometimes it requires multiple interactions to serve a request. Take for example the card authorization use case: - Optimistically post transaction - Check if any balances are violated - Void if one of the balances is violated - Indicate in response if transaction is Approved or Declined There could possibly be 2-3 network calls to Twisp to fulfill this use case with just the GraphQL api interacting with your application. However, if you want to accomplish all of this _transactionally_ in Twisp, we support defining state machines using [States Language](https://states-language.net/) that allows you to cooridnate multiple api calls. In the following example: - post a `CARD_HOLD` optimistically to a card account - check the balance of the parent deposit account set - if the balance is less than zero we void - return either a `Declined` or `Approved` message based on the balance > **Tip:** > > This feature is not yet GA but we're looking to ship in the next few days. **Request** ```graphql mutation PostCheckAndVoid( $authorizationVariables: JSON! $machine: StateMachine! ) { workflow { executeStatesLanguage( input: { input: $authorizationVariables, machine: $machine } ) { output } } } ``` **Response** ```json { "data": { "workflow": { "executeStatesLanguage": { "output": { "message": "Declined", "variables": { "balance": "-291.00", "hasBalance": true, "shouldVoid": true, "transactionId": "772269e7-4c11-4375-bc3d-587fed54b2ea", "voided": { "voidTransaction": { "transactionId": "a97b893f-cf69-54f8-ac51-afa5d6f789ff" } } } } } } } } ``` **Variables** ```json { "machine": { "Id": "c4edf3bd-ae11-490f-98bc-f3d9547bc516", "Name": "PostAndVoid", "AwsStatesLanguage": { "Comment": "Post a transaction and void if balance is lt zero.", "StartAt": "Post", "States": { "Post": { "Type": "Task", "Resource": "arn:twisp:workflow:::financial/v1/graphql", "Parameters": { "query": "mutation Auth(\n $authorizationId: UUID!\n $accountId: UUID!\n $correlation: String\n $amount: Decimal!\n $effective: Date\n){\n postTransaction(\n input:{\n transactionId: $authorizationId\n tranCode: \"CARD_HOLD\"\n params: {\n account: $accountId\n amount: $amount\n correlation: $correlation\n effective: $effective\n metadata: \"{}\"\n }\n }\n) {\n transactionId\n}}\n", "variables": { "authorizationId.$": "$.authorizationId", "accountId.$": "$.accountId", "amount.$": "$.amount", "correlation.$": "$.correlation", "effective.$": "$.effective" } }, "ResultSelector": { "transactionId.$": "$.postTransaction.transactionId" }, "ResultPath": "$.variables.posted", "Next": "CheckBalance" }, "CheckBalance": { "Type": "Task", "Resource": "arn:twisp:workflow:::financial/v1/graphql", "Parameters": { "query": "query CheckBalance($accountId: UUID!) {\n account(\n id: $accountId\n ) {\n sets(first:1) {\n nodes {\n sets(first:1) {\n nodes {\n balance {\n available(layer:PENDING) {\n normalBalance {\n units\n }\n }\n }\n }\n }\n }\n }\n }}\n", "variables.$": "$.variables" }, "ResultPath": "$.balance", "Next": "FormatResult" }, "FormatResult": { "Type": "Task", "Resource": "arn:twisp:workflow:::cel", "Parameters": { "variables": { "hasBalance": "size(context.workflows.this.CheckBalance.balance.account.sets.nodes) > 0 && size(context.workflows.this.CheckBalance.balance.account.sets.nodes[0].sets.nodes) > 0", "balance": "this.hasBalance ? context.workflows.this.CheckBalance.balance.account.sets.nodes[0].sets.nodes[0].balance.available.normalBalance.units : decimal('0')", "shouldVoid": "decimal(this.balance) <= decimal('0')", "transactionId": "context.workflows.this.CheckBalance.variables.posted.transactionId" } }, "Next": "ShouldVoid" }, "ShouldVoid": { "Type": "Choice", "Default": "Approved", "Choices": [ { "Variable": "$.variables.shouldVoid", "BooleanEquals": true, "Next": "Void" } ] }, "Void": { "Type": "Task", "Resource": "arn:twisp:workflow:::financial/v1/graphql", "Parameters": { "query": "mutation VoidTransaction($transactionId: UUID!) {\n voidTransaction(id: $transactionId) { transactionId }\n}\n", "variables.$": "$.variables" }, "ResultPath": "$.variables.voided", "Next": "Declined" }, "Approved": { "Type": "Pass", "Parameters": { "message": "Approved", "variables.$": "$.variables" }, "End": true }, "Declined": { "Type": "Pass", "Parameters": { "message": "Declined", "variables.$": "$.variables" }, "End": true } } } }, "authorizationVariables": { "accountId": "f0e4d71e-fc44-41ea-8e10-bd4b039885f1", "amount": "10.00", "authorizationId": "772269e7-4c11-4375-bc3d-587fed54b2ea", "correlation": "2f05a4d4-07c5-42c1-a567-438737b7a0ab", "effective": "2022-10-01" } } ``` ### Custom Balance Computations By default Twisp rolls up balances on a number of dimensions: - journal - account - currency Twisp offers the ability to compute balances on additional dimensions. For example, often fintechs will put limits in per MCC code or perhaps on daily, weekly, monthly or yearly spending limits. These are supported in Twisp via [Balance Calculations](/docs/reference/graphql/mutations#create-calculation). Once you've created a balance calcuation, you may look up balance on that calculation via the balances endpoint by supplying the `calculationId` and `dimension` values you're interested in. For example, for the above calculation: ```graphql query GetJan12020EffectiveBalance($accountId: UUID!) { balance( accountId: $accountId currency: "USD" calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" dimension: { effectiveDate: "2020-01-01" } ) { available(layer:PENDING) { normalBalance { units } } } } ``` --- # Twisp The accounting engine for financial products. Source: https://www.twisp.com/docs Welcome to the Twisp Docs. Here you can read about what the Twisp Accounting Core is, why it works the way that it does, and how you can interact with it via the GraphQL API. - [**Introduction**](/docs/introduction)\ Get familiar with the basics of what Twisp is and why we built it. - [**Accounting Core**](/docs/accounting-core)\ Explore the features in the accounting core: ledgers, transactions, accounts, and more. - [**Processors**](/docs/processors)\ Process external payment networks and financial protocols, handling communication with ACH, wire transfers, and card networks. - [**Infrastructure**](/docs/infrastructure)\ Read about the underlying systems that give Twisp a unique technological edge. - [**API Reference**](/docs/reference)\ Browse the endpoints and type schema made available through our GraphQL API. --- # Computation Engine The computation infrastructure supports protocol workflows and an extensive financial calculation runtime via CEL. Source: https://www.twisp.com/docs/infrastructure/computation-engine Protocol workflows provide the ability to model sophisticated state-machine-like computations on ledger data and calculations, including the ability to communicate with external systems. With the workflow engine, it is possible to model sophisticated financial protocols and their interaction with ledgers. Examples of these protocols include ISO 8583, ACH, and external webhook processing. Finally, Twisp's built-in expression language, which is highlighted later in this document, is populated with an extensive function library. In addition to a traditional standard library, Twisp also includes advanced financial functionality to assist in complex financial systems. ## Workflows as State Machines A state machine is an abstraction used to design algorithms and protocols. A state machine simply reads a set of inputs and changes to a different state based on those inputs. A state is a description of the status of a system waiting to execute a transition. Twisp provides a mechanism to define state machines called protocol workflows. The combination of immutable ledgers, the calculation runtime, and protocol workflows allows Twisp to process all manner of complex financial protocols and translate them into ledger entries with full historical lineage and integrity guarantees. The input for protocol workflows can be triggered by events resulting from ledger operations (create, read, update, delete) or API calls to Twisp (including webhooks from external systems). One can think of the operational trigger events for protocol workflows with the same mental model of triggers in monolithic relational databases. Workflows in Twisp occur in the context of the same atomic interactive transaction that triggered them. This provides strong transactional and data integrity guarantees for the resulting data. The output from workflow transitions and the resulting state from the calculation engine is also persisted to ledgers. This allows Twisp to define chains of transactionally guaranteed protocol workflow computation with full lineage. Since each transaction in Twisp has dedicated compute and memory, there is little concern of noisy neighbor problems or node overloading. ## Calculation Runtime The calculation runtime is the primary mechanism Twisp uses to both populate ledgers and derive useful information from ledger data. It is an expression based language and runtime that allows processing over schema-based input and that results in schema-based output. It is purpose-built to operate with all manners of persisted ledger data with strong integrity guarantees including a powerful type system, strong type checking, and predictable runtime performance. Calculation expressions are powered by [Common Expression Language (CEL)](https://github.com/google/cel-spec) along with our financial type system and function library. This provides an expressive and powerful mechanism to derive and populate ledger data. All data resulting from expressions in the calculation runtime have strong integrity guarantees, and result in strictly-typed schema-based output that is purpose built for persisting to ledgers. The calculation engine supports use cases such as calculating balances, fees, and interest on any dimension; extracting, aggregating, and bucketing ledger data; real-time analytics for business intelligence or user insights; and all manner of user defined functionality powered by the expressive syntax and extensive function library. ## Function Library The ledger core and calculation runtime plus standard library and protocol workflow provide an extremely powerful set of primitives which lie at the foundation of the Twisp accounting core. To make these primitives even more powerful for financial applications, Twisp includes a financial type system and extensive function library. This functionality is surfaced within the calculation runtime as types within the strict type system and functions that operate over those types within expressions. The Twisp financial type system and function library includes sophisticated types such as full-featured money and currency types, along with functions to make complex calculations on those types, such as interest, fees, rounding, balances, and the like. The function library also includes parsers for well known external formats, such as ISO8583, ACH, and many more. --- # Infrastructure Underneath the accounting core is a purpose-built ledger database and computation engine. Source: https://www.twisp.com/docs/infrastructure ## Architectural Components There are four primary components to the infrastructure which powers the Twisp Accounting core: * The [Financial Ledger Database](/docs/infrastructure/ledger-database) is a cloud-native, append-only ledger data store with strong guarantees about the lineage of data. * The [Computation Engine](/docs/infrastructure/computation-engine) powers massively scalable data operations, protocol workflows, and an extensive financial calculation runtime. * [Security configurations](/docs/infrastructure/security-and-auth) that provide strong access controls through explict authorization policies. * The [Local Environment](/docs/infrastructure/local-environment) streamlines development and CICD integration testing. ## Background: Decoupling Storage, Computation, and Security In most modern systems, data storage is decoupled from computational processes. To manage storage of this data, applications contain logic to replicate storage data structures, apply transactional logic, and then execute remote calls to persist the resulting data in external, decoupled storage systems. Decoupled storage and computation is ubiquitous in nearly all distributed architectures. This decoupling of storage and computation makes the process of guaranteeing the integrity of a value calculated in one system, then persisted in another, into a complex distributed systems problem. For financial ledger systems where we must take great care to guarantee not only the integrity of financial data but also calculations that result from that data, this is a problem. However, with Twisp, just as referential integrity offers integrity over relations, the financial ledger database offers integrity, full transparency, and lineage of both the underlying data along with all calculations derived from that data. This property allows us to declaratively define strict, well-defined, and atomic processes that compute ledger calculations, and with those procedures defined, the underlying system transactionally guarantees the accuracy of the resulting calculation data. Additionally, we treat security and authentication/authorization concerns as first-class citizens with their own dedicated resources. All access controls are determined by a centralized set of security policies. There are two primary reasons for this design: 1. Keeping all security protocols in a single place makes it easy for auditors to review every policy and quickly identify any areas of concern. 2. Storing policies in separate locations runs the risk of introducing conflicts between policy definitions and causing unexpected errors. --- # The Twisp Financial Ledger Database The ledger database is an immutable, append only data structure that provides strong gaurantees about the lineage of data. Source: https://www.twisp.com/docs/infrastructure/ledger-database ## Overview: Ledgers à la Twisp At Twisp, we set out to rethink the underlying technology for financial ledger systems by combining the operational and scaling characteristics of a distributed database with the correctness guarantees offered by relational databases. The result is the **Twisp Financial Ledger Database** (FLDB). The Twisp ledger database is transactionally guaranteed, secure, and highly scalable; it is tailor-built for financial system use cases. It is designed to easily support the massively multi-tenant workloads that modern financial applications require. With the advent of serverless and infrastructure-as-code, the ability to define fully functioning systems with high-level code is now a reality. Twisp uses an infrastructure-as-code approach to creating resources and managing access to those resources. ## Comparison to Other Databases Like traditional relational databases, the Twisp FLDB enforces the structure of data with strict user-defined schemas, indexes, and referential integrity. Users familiar with the tabular model of SQL will find many of the same principles and abstractions apply. The internal structure of the database is not limited to tabular data, however. Like document databases, ledgers records can contain nested sub-documents as well as collection data types like lists and JSON. Sub-documents still must conform to a strictly defined schema. In this way, the database supports both modalities of data modeling: tabular and document-based. Because schema migrations are so simple, it is easy to adapt the data model as new requirements emerge. ## Database Features ### Append-Only Immutability The lineage of data in a financial system is critical. When it comes to monetary values, we need to be able to track with confidence where the money came from, where it went, and how a balance grew from $0 to $1. At the most basic level of the database structure, we can make these same guarantees about any data that enters the system. We do this through a simple mechanism: immutability. Immutability means that for an operation that results in new data, the system simply records that new piece of data. All previous data still remains recorded unchanged as well. You can think of this as a log of all changes to a system. Once we have this append-only log, we can then apply strong cryptographic guarantees to the log so the underlying data remains tamper proof and unchanged. We call this log and strong cryptographic guarantees an immutable ledger of changes to the system. Twisp provides this as a primitive to make use of these same guarantees for their application data. Therefore, all data are recorded with full lineage in the system. In practice, this means that a `history` node is available on every record in the database to expose the chain of changes to the state of any record. Read more about append-only immutability at [Versions And History](/docs/reference/ledger/versions-and-history). ### Strong Data Integrity A transactional MVCC write-ahead log (WAL) tracks all data that is managed by the system. The data blocks of the WAL are chained together with cryptographic hash functions. This cryptographically sealed chain allows Twisp to verify the integrity of any data in the system using cryptographic verification. The system utilizes a digest hash value that represents the full hash chain of the WAL at any committed value plus a Merkle audit proof to cryptographically verify the integrity of any data in the system. This allows users to verify and prove the integrity of any record in the database at any time. ### Index-First Design All reads to data stored must occur via an index. This eliminates an entire class of problems that can occur in production systems with table scans and unclear query planning, and promotes well-understood data access patterns. There is no limit the number of indexes on data, as we recognize the tradeoff that more indexes allows for the possibility of write amplification. The index system is core to Twisp’s infrastructural design, allowing for massive scalability and zero-downtime migrations. Index features: - **Strongly consistent**\ All database operations through indexes guarantee data integrity. - **Strong partition and sorting controls**\ The declarative schema supports compound keys and multi-field sorting. - **Compatible with all fields**\ Any part of the schema can be used to create the index key, including the JSON and List collection types. - **Filterable with CEL expressions**\ Partial indexes can be created to index only those records matching a specified condition. - **Unlimited definitions**\ Ledger definitions can include as many indexes as needed. - **Online, zero-downtime migrations**\ Indexes can be re-partitioned on the fly using the migration system. ### Index-Based Relations Using properly constructed indexes, ledgers support all the standard relation types: - One-to-one - One-to-many - Many-to-many The database implements full referential integrity: records can refer to other records via an index, even across ledgers. Twisp ensures that a referenced (parent) record cannot be deleted without any referencing (child) records first being deleted. In addition to preserving referential integrity, the GraphQL schema will automatically support join operations between referenced ledgers. ### Referential Integrity A powerful feature of relational databases are tools to enforce the integrity and structure of the data they store. Strict schemas require a homogenous shape to all data stored in a table. Rather than depending on an application to enforce a data schema, the database enforces it instead. With this, the database system can now run migrations and schema changes to reorganize the data into evolving structure for you, with strong guarantees that data is always in a well known shape. In addition to the schema structure of individual data elements, referential integrity can enforce the relationships between different related pieces of data. The relationships between these pieces of data are often referred to as foreign key (FK) constraints. These constraints provide a mechanism that allows a data element to reference another separate but related piece data via a FK relationship. Once this relationship is established the system guarantees that one piece of data that is in relation to another will always exist by preventing any changes, whether updates or deletes, that might violate the relationship. The system is maintaining referential integrity. ### Type-Safe Schemas Within the database, every record conforms to the same schema of typed fields defined within a declarative schema. Fields can be typed to contain a single **scalar** type like strings or signed integers, **collection** types like lists or JSON objects, or they can contain **sub-documents** with their own fields. This table lists all currently supported types for ledger fields. | Type | Description | |-------------|-------------------------------------------------------------------------| | `int` | 64-bit signed integer | | `uint` | 64-bit unsigned integer | | `double` | 64-bit IEEE floating-point number | | `bool` | boolean value true or false | | `string` | Strings of Unicode code points | | `timestamp` | ISO-8601 timestamp with nanosecond precision and full time zone support | | `bytes` | Byte sequences (base64 encoded) | | `uuid` | 128-bit RFC4122 Universally Unique Identifier | | `json` | holds arbitrary JSON (use "{}" to structure insert values) | | `[]` | list of scalar values (example: [string]) | Note the two collection types `json` and **list** (`[]`). Lists can be used to store variable-length ordered collections of a scalar types. `json` types can store arbitrary JSON documents (up to X bytes) Twisp combines the flexibility of document databases like MongoDB while maintaining the schematic integrity and operational model of a table-driven relational database like PostgreSQL. In practice, this means that you can create nested document structures while maintaining type safety and predictable data shapes. Sub-documents can be used to store related fields that might otherwise add noise to the base document. Because they are typed as well, sub-documents are part of the explicitly declared schema and behave in just the same way as fields in the base-level document do. They can even be used in indexes. ### Zero-Downtime Migrations With declarative schemas we can simply change the schema definition and Twisp will create, validate, and execute changesets to migrate your schema from version to version, all in an online fashion with zero down time. > **Note:** > > Zero downtime means that existing resources can continue to be accessed and used. Newly added/removed elements will be in a `DELETE_ONLY` or `WRITE_ONLY` state as they're added to the system and in some cases backfilled with data. The table below shows all supported operations for migrations. | Migration | Support | |----------------------|----------------------------------------| | Add | Supported | | Drop | Supported | | Add Index | Supported | | Drop Index | Supported | | Add Schema Elements | Supported | | Drop Schema Elements | Supported | | Add References | Supported | | Drop References | Supported | | Add Joins | Supported | | Drop Joins | Supported | | Add Calculation | Supported, replays based on `Position` | ### Transaction Layer The transaction layer provides MVCC interactive transactions for database operations in the system. It allows for strictly serializable transaction isolation levels for the strongest consistency guarantees. The interface to the transaction layer is provided via transaction scopes. Transaction scopes provide an interface to the transaction layer and a clear mental model for determining how transactions for specific data should interact. --- # Local Environment The local environment helps streamline development and CICD integration testing with Twisp running on your own machine. Source: https://www.twisp.com/docs/infrastructure/local-environment ## Starting the environment The following shell commands can be used to get up and running: ```sh docker pull public.ecr.aws/twisp/local:latest docker run -p 3000:3000 -p 8080:8080 -p 8081:8081 public.ecr.aws/twisp/local:latest ``` You should see the startup logs in your terminal. The console should now be available in the browser at . ## Work through the Twisp tutorial After starting up your local environment check out the Twisp [tutorial](/docs/tutorials/twisp-101) for more information on using Twisp. ## Environment details The local Twisp container image includes a number of services and data persistence options. Container images are available for both `amd64` and `arm64` architectures. ### Available services - HTTP console port `3000` - HTTP API port `8080` - GRPC API port `8081` - Health check command `/healthcheck` > Note: the Twisp Local Instance does not require HTTP `Authorization` headers for access. ### Data persistence - Set `DB_PATH` environment variable to `-e DB_PATH=/data` - Mount a volume: `-v volume:/data` ### Account management - Execute multi-tenant API calls by posting with a specific `X-Twisp-Account-Id` HTTP header - If an `X-Twisp-Account-Id` header is not provided, the system will default to `X-Twisp-Account-Id: 000000000000` - The console invokes the API on behalf of the default account --- # Security and Auth Security policies control access to encrypted resources. Source: https://www.twisp.com/docs/infrastructure/security-and-auth Ensuring the proper security of data is foundational to the Twisp infrastructure. Out of the box, data is [securely encrypted both at rest and in transit](#encryption) and all access is authenticated via JWT tokens issued by the [OpenID Connect 1.0 protocol](https://openid.net/connect/). Users can configure additional access settings by defining [tenants](#provisioning-tenants) and [clients](#creating-clients-and-policies) through GraphQL mutations to set precise permissions on specific operations. ## The authentication & authorization flow Twisp applies policies to an authenticated principal before allowing access to resources. In a simplified form, the stages for the authentication process are: 1. A principal issues an HTTPS request with their OIDC JWT and Twisp account IDs in the headers. 2. The principal name is extracted from the JWT. 3. Twisp finds the corresponding policies to apply using the principal name. 4. The resulting policies are evaluated for authorization. ### Making an authenticated request All HTTPS requests to the [GraphQL API](/docs/reference/graphql) must provide the following headers to allow for authorization: ``` Authorization: Bearer x-twisp-account-id: ``` The JWT can be issued either with OpenID Connect or AWS IAM. ### Authorizing a principal Within a client configuration, the **security principal** is the authenticated identity accessing the system. The client authorization system retrieves [policies](#creating-clients-and-policies) based on the principal name. For OIDC this is typically the issuer found in the `iss` claim. For AWS IAM it is the IAM role, user, or other AWS identity placed in the `sub` claim when Twisp vends a token. ## Provisioning Tenants Each cloud environment requires its own tenant. Use the admin GraphQL namespace to create tenants from an existing account: ```graphql mutation CreateTenants { admin { staging:createTenant( input: { id: "d9d8f1c0-0299-4d5b-b2b6-85beafdda28b" accountId: "TwispStaging" name: "staging" description: "staging environment for Twisp" } ) { accountId name } production:createTenant( input: { id: "848df974-4133-4ee4-ab45-86e5e29b6822" accountId: "TwispProd" name: "production" description: "production environment for Twisp" } ) { accountId name } } } ``` Once a tenant exists, clients may be created within that tenant to govern access. ## Creating Clients and Policies Authentication to Twisp works with any OIDC-compliant token supplied through the `Authorization: Bearer ` header. A corresponding client must exist in Twisp for the principal identified in the token so Twisp knows which policies to apply. ### Third-party OIDC client example For tokens issued by an external identity provider, set the client `principal` to the OIDC issuer URL (`iss` claim). The following mutation creates per-user policies for tokens issued by Google: ```graphql mutation CreateGoogleCloudClient { auth { createClient( input: { name: "michael gcloud readonly" principal: "https://accounts.google.com" policies: [ { actions: [SELECT] resources: ["*"] effect: ALLOW assertions: { isMike: "context.auth.claims.email == 'michael@twisp.com'" } } { actions: [SELECT, INSERT, UPDATE, DELETE] resources: ["*"] effect: ALLOW assertions: { isJarred: "context.auth.claims.email == 'jarred@twisp.com'" } } ] } ) { principal } } } ``` ### AWS IAM principal example Twisp can exchange a presigned AWS STS `GetCallerIdentity` request for an OIDC token. The resulting token uses the AWS identity as the principal. Create a client for that principal to grant access: ```graphql mutation CreateIAMAuthClient { auth { createClient( input: { principal: "arn:aws:iam::012345678901:role/example-role" name: "example role policy" policies: [ { effect: ALLOW actions: [SELECT, INSERT, UPDATE, DELETE] resources: ["*"] } ] } ) { principal } } } ``` ### Understanding policies Each policy defines an `effect` (`ALLOW` or `DENY`), the `actions` permitted or denied, the `resources` in scope, and optional CEL `assertions` that must evaluate to `true`. > **Logical combination of policies** > > Policies are evaluated as a chain of logical `AND` statements scoped to the requested resource and action. A principal must have *at least one* `ALLOW` policy for a resource/action pair. A single `DENY` policy on that pair blocks the operation. Values for `resources` and `actions` support `*` and `?` wildcards. Actions include: - `db:Select` (`SELECT` in GraphQL) : read a document - `db:Insert` (`INSERT`) : create a document - `db:Update` (`UPDATE`) : change document fields - `db:Delete` (`DELETE`) : remove a document Resource identifiers follow the format `namespace.ledger..propertyName`. The current namespace is `financial`. Assertions use [Common Expression Language (CEL)](/docs/reference/cel) and can access `context.auth.claims` (token claims) and `context.document` (the document being acted on). If any assertion evaluates to `false`, the policy is skipped. ## Using OpenID Connect tokens Any JWT generated by an OpenID Connect 1.0 capable issuer is supported by Twisp for authentication. When an API endpoint receives the JWT, it validates the token signature against the issuer and—if valid—invokes the endpoint with a security principal set to the issuer `iss` claim. All claims are embedded in `context.auth.claims` for policy evaluation. ## Issuing tokens with AWS Identity and Access Management (IAM) Twisp can vend an OpenID Connect token in exchange for an authenticated AWS IAM role or user. This allows services running in AWS to retrieve a Twisp-issued token for access. The issuer of these tokens is `https://auth.${AWS::Region}.prod.twisp.com/token/iam`, and the resulting principal name equals the original IAM identity stored in the token `sub` field. Twisp also provides a hosted service to exchange a presigned `GetCallerIdentity` request for a token, making it convenient to obtain OIDC tokens inside AWS environments. ## Cloud endpoints - AWS Twisp Token: `https://auth.us-east-1.cloud.twisp.com/token/iam` - Financial GraphQL: `https://api.us-east-1.cloud.twisp.com/financial/v1/graphql` - gRPC: `https://api.us-east-1.cloud.twisp.com:50051` ## Encryption ### Encryption in transit Data is encrypted in transit via HTTPS connections for all external and internal API operations. This protects against man-in-the-middle attacks and prevents eavesdropping on Twisp traffic. ### Encryption at rest All data stored in Twisp is encrypted at rest. Encryption keys are stored in [AWS Key Management Service](https://aws.amazon.com/kms/), so even if a malicious actor gains access to the storage layer they cannot read the data without the proper keys. --- # System Architecture Twisp is a purpose-built system that provides a mission-critical, high performance, core ledger for constructing and running financial products, empowering organizations to have complete control over their crucial financial data. Source: https://www.twisp.com/docs/infrastructure/system-architecture ## Integrating with Twisp Integrating with the Twisp Accounting Core streamlines financial data flows, structures ledger data, automates financial processes, and helps you design and build seamless financial products. In this document, we will discuss how to integrate your system with Twisp, covering common components in your stack and the interfaces available to connect those components to the Twisp Accounting Core. [Architecture diagram available in the HTML documentation.] ## Your System Your system is the foundation for your integration with Twisp. It includes the frontend, infrastructure, services, and vendors that you interact with. ### Frontend Your frontend is the user interface that your customers use to interact with your system. By combining fine grained security policies with OIDC authentication, it's possible to use our GraphQL API directly from frontend applications. This provides a scalable and performant mechanism to enable your customers to view and interact with their financial data stored in Twisp's ledger database. ### Infrastructure Your infrastructure is the backbone of your system. It includes servers, streaming platforms, databases, warehouses, and networking components. These lower-level technology components often require access direct access to financial data stored in the Twisp Ledger Database. Real-time streaming and batch access to this data is provided by Twisp data connectors. ### Services Your services are self-contained, loosely coupled software components that provide specific functionality to power your business. To integrate with Twisp, you'll need to identify the specific financial services that these systems provide, such as brokerage, checking, savings, credit, or loan products. The logic for that product is then setup in the Twisp Accounting Core, and services then integrate directly via Twisp API interfaces with GraphQL, gRPC, or REST for data exchange. ### Vendors Your vendors are the third-party tools and services that you use to support your system. Twisp has an expanding library of pre-built vendor and protocol integrations that are connected directly to the Accounting Core. This tooling allows simple plug-and-play integration of your Accounting Core with existing investments. ## Interfaces Interfaces are the methods by which your system interacts with Twisp. They include the API, data, integrations, and protocols that enable communication between your system and Twisp's accounting engine and ledger database. ### API Twisp includes a number of modern API interfaces, allowing your teams to integrate with technologies best suited to your organization. | Technology | Description | |---|---| | GraphQL | GraphQL is an open-source query language for APIs (Application Programming Interfaces) and a runtime for executing those queries with existing data. It was developed by Facebook and released in 2015. GraphQL enables clients to request specific data from a server, allowing them to retrieve only the information they need and nothing more. | | gRPC | gRPC (Google Remote Procedure Call) is an open-source high-performance framework developed by Google. It allows you to define services and message types using Protocol Buffers (protobuf) and enables efficient communication between distributed systems. | | REST | REST (Representational State Transfer) is an architectural style for designing networked applications. It provides a set of principles and constraints for building web services that are scalable, stateless, and interoperable. Additionally, the OData protocol allows Twisp to easily integrate with services such as Salesforce out of the box. | ### Data Twisp provides various data integration interfaces to facilitate the seamless processing and transfer of data. These interfaces cater to both real-time and batch scenarios, allowing you to effectively work with data in Twisp. #### Real-Time Data Integration Real-time data integration involves the continuous or near-instantaneous processing and transfer of data between systems as it becomes available. Twisp supports the following real-time data interfaces: * **Streaming**: Streams provide near real-time change data capture capabilities, allowing you to stream data to various technologies such as AWS Kinesis, Kafka, or webhooks. This ensures that you can stay up-to-date with the latest changes in your data. * **Webhooks**: Twisp supports webhooks for real-time HTTPS data integration. You can configure webhooks to receive instant notifications or data updates from external systems, enabling seamless communication and data synchronization. #### Batch Data Integration Batch data integrations involve processing and transferring data in bulk at scheduled intervals or on-demand. Twisp supports the following batch data interfaces: * **SQL**: The SQL interface enables you to perform ad-hoc queries for generating reports, exporting data, or integrating with popular business intelligence tools like Tableau or PowerBI. * **Warehousing**: Twisp aggregates all committed financial data in near real-time and stores it in the industry-standard [Parquet](https://parquet.apache.org) file format. This format is compatible with leading data warehousing systems like Redshift, Snowflake, or BigQuery. You can easily ingest this data into your preferred data warehousing system for further analysis and reporting. By leveraging these data integration interfaces in Twisp, you can effectively manage your data, whether it's processing real-time updates or performing bulk transfers at your convenience. ### Integrations Integrations are the connections between the Accounting Core and other financial systems and business tools. Twisp includes an expanding number of plug-and-play integrations. If an integration you are interested in is not included in this list, please reach out and we can prioritize development. | Vendor | Description | |---------|-----------------------------------------------------------------------| | Fiserv | Twisp can integrate with a number of Fiserv core systems. | | Lithic | Twisp natively processes Lithic transaction webhooks, including ASA. | | Marqeta | Twisp natively processes Marqeta transaction webhooks, including JIT. | | Stripe | Twisp natively processes Stripe Issuing and Treasury webhooks. | ### Protocols Protocols are industry-standard rules and formats that govern structured communication between financial systems. Our platform includes plug-and-play protocol adapters that process, store, and generate data while adhering to strict security and compliance standards. If you're interested in a protocol not listed below, please contact us, and we will prioritize its development. | Protocol | Description | | --- | --- | | ISO 8583 | Seamlessly integrate with several card networks, directly consuming ISO 8583 messages and converting them into transaction entries within our platform. | | ISO 20022 | Process ISO 20022 messages, encompassing Swift and FedNOW transactions. Additionally, generate ISO 20022 messages from ledger data. | | NACHA | Proess and generate NACHA ACH files. | | X9 | X9 Cash image files serve as the standard method for check clearance in the US. Twisp can efficiently consume X9 files and track check transactions in their native format. | | BAI2 | Comprehensive support for financial institutions generating or processing BAI2 files. | ## Core The Twisp Accounting Core is the heart of your integration with Twisp. It includes the accounting engine, ledger database, product logic, and security policies that enable secure, reliable, and compliant financial operations. ### Accounting Engine The accounting engine is the core of the Twisp Accounting Core. It includes the chart of accounts, transaction codes, and ledger entries that enable processing and accounting for financial transactions. ### Ledger Database The ledger database is where your financial data is stored within Twisp. Twisp is built on a production hardened, high performance, horizontally scalable database storage system: [AWS DynamoDB](https://aws.amazon.com/dynamodb). The ledger database leverages DynamoDB using a formally verified transaction layer, which provides transactionally consistent, snapshot isolated, immutable storage, providing a complete historical lineage of any object. ### Product Logic Product logic is the essential business logic that powers your financial products and services. With Twisp, you have complete control over product development in the Accounting Core, allowing you to design and create a wide range of financial products. Twisp also offers pre-built financial product definitions that can be customized or extended according to your specific requirements. | Product | Description | | --- | --- | | Brokerage | Brokerages have unique accounting needs, including the representation of numerous positions within a single account. Twisp enables you to execute the trading lifecycle and reconcile trades with a clearinghouse, all while operating at high scale during market hours. | | Cards | Simplify your card issuing program by leveraging Twisp's card issuing workflows and transaction codes. Twisp integrates seamlessly with issuer processors, providing robust accounting control over the cards you issue. You can enable features such as spend limits, wallets, and organizational structures tailored to your needs. | | Credit | Credit card programs require specific functionalities beyond debit cards. Twisp's basic card program can be enhanced with SCRA processes, available credit balance tracking, and customizable interest rate computations. | | Deposits | Twisp supports various deposit-taking modalities. Whether you offer a card product on top of FBO (For Benefit Of) accounts or provide demand deposit accounts (DDAs), Twisp's deposit program can be tailored to suit your requirements. | | Lending | Enhance your origination and servicing capabilities by incorporating Twisp's lending components. Account for payments, generate or regenerate amortization tables, and enable portfolio monitoring directly from the accounting core. | | Mortgages | Manage mortgage accounting processes for origination and servicing directly from the Accounting Core. | | Payments | Twisp's multi-currency design enables you to manage global payments accounting with FX (Foreign eXchange.) | | Wallets | Simplify the management of your organization's spend program with Twisp's powerful wallet management. Configure your organizational hierarchy, assign spend limits, and Twisp's wallet program will effectively manage spending. If you choose to issue cards associated with wallets, Twisp seamlessly integrates with various card module partners to provide the necessary capabilities. | ### Security Policies Security policies are the rules and procedures that ensure the security and compliance of your system and financial data. Twisp supports unlimited fine grained access control policies that authorize all operations executed against the accounting core. Additionally, Twisp is a credential-less system, leveraging your existing identity or cloud providers identities and roles for all authentication. --- # Transaction Isolation and Consistency Twisp provides snapshot isolation and strong consistency to ensure correctness and prevent anomalies in ledger systems. Source: https://www.twisp.com/docs/infrastructure/transaction-isolation-and-consistency ## Transaction Isolation in Twisp In systems that demand correctness and high concurrency, transaction isolation plays a critical role in ensuring consistency and preventing anomalies. Twisp leverages an ACID **Multi-Version Concurrency Control (MVCC)** transaction layer to implement robust isolation levels tailored to the needs of ledger systems. Twisp's MVCC provides: - **Interactive transactions**: Supports all-or-nothing semantics for transactions, ensuring atomicity and isolation. - **Snapshot Isolation**: Transactions operate on a consistent and isolated view of data, ensuring transactions operate on immutable data snapshots. - **Anomaly prevention**: Guarantees that transactional operations produce fully consistent results without partial or invalid reads along with write skew prevention. - **Strong consistency**: All committed balances and database states are always readable in a strongly consistent manner, ensuring no anomalies or partial updates. > **Note:** > > Twisp runs and maintains a formal model of our MVCC transaction layer. Learn more about the formal modelling process we conducted with our partner [Galois](https://galois.com/blog/2024/02/galois-twisp-avoiding-foolishness-in-distributed-systems/). ### Isolation Levels Overview Twisp supports four distinct transaction isolation levels, balancing between performance and correctness: 1. **Read Committed**: - Ensures that the most recently committed data is visible to a transaction. - Use cases: Workflows requiring real-time, consistent reads that can tolerate a non-snapshot view of data. 2. **Snapshot Isolation (System Default)**: - Guarantees that transactions operate on a point-in-time consistent snapshot of the data. - Use cases: Workflows requiring strong correctness without paying a performance penalty. 3. **Repeatable Read**: - Guarantees that read data has not changed on transaction commit providing read stability. - Use cases: High-consistency workflows for complex, interactive, multi-row updates. 4. **Serializable**: - In addition to read stability, also provides phantom avoidance that ensures scans would not return additional data on transaction commit. - Use cases: Workflows where absolute correctness outweighs performance concerns. > **Note:** > > Transaction isolation settings can be set with the [`@tx` directive](/docs/reference/graphql/directives#tx) in GraphQL. ### Preventing Transaction Anomalies In high-concurrency environments, transaction anomalies can corrupt data or lead to business-critical errors. Twisp's isolation levels guard against common database anomalies. The following anomalies are thoroughly tested in every build via our conformance test suites: - **G0**: Write Cycles (dirty writes) - **G1a**: Aborted Reads (dirty reads, cascaded aborts) - **G1b**: Intermediate Reads (dirty reads) - **G1c**: Circular Information Flow (dirty reads) - **OTV**: Observed Transaction Vanishes - **PMP**: Predicate-Many-Preceders - **P4**: Lost Update - **G-single**: Single Anti-dependency Cycles (read skew) - **G2-item**: Item Anti-dependency Cycles (write skew on disjoint read) - **G2**: Anti-Dependency Cycles (write skew on predicate read) By addressing these anomalies, Twisp's isolation levels ensure correctness and make ledger operations easy to reason about, even under high concurrency. ## Strongly Consistent for All Operations Twisp ensures **strong consistency** across all database operations, guaranteeing that every transaction reflects an accurate state of the ledger. This robust consistency model removes ambiguity, preventing the risks of stale or partial reads. > **Note:** > > Customer defined __search__ index documents are replicated in an eventually consistent manner to [OpenSearch](https://opensearch.org/). ### Balance Calculations For high-volume, concurrent entry posting to concurrent-enabled accounts or aggregates, Twisp implements a non-blocking, transactionally consistent, and deterministic balance update process. The following balance types are provided to allow context-specific balance retrieval consistency without compromising correctness. | **State** | **Description** | **Use Case** | |-----------|-----------------|--------------| | **Provisional** | Includes in-flight, uncommitted transactions along with committed changes. | Enforce velocity controls on to-be-committed transactions. | | **Prepared** | Combines finalized balances with committed, but not-yet-finalized entries. Guaranteed to provide the exact same results as the final balance. | Systems requiring transactionally consistent balances on concurrent-enabled accounts. | | **Final** | Reflects the fully committed and finalized state of the account. Finalized balances are cached. | Systems that require the highest performance balance reads and can tolerate slightly stale balance values. | > **Note:** > > For concurrent-disabled accounts, the `Prepared` balance state is skipped and the balance update process proceeds directly to `Final` from `Provisional`. --- # A Short History of Twisp Why we did it. Source: https://www.twisp.com/docs/introduction/history Monolithic relational databases are the traditional foundation of financial core ledger systems. Nevertheless, the process of building and operating mission-critical financial ledgers on these databases is a journey fraught with engineering challenges. In addition, the databases underlying these systems are often difficult to operate at scale and a poor choice for multi-tenant systems. Distributed database technologies have proven themselves to solve these operational issues. However, **the design choices that allow these systems to achieve horizontal scale make them an even more complicated fit for financial ledgers.** At Twisp, we set out to rethink the underlying technology for financial ledger systems by combining the operational and scaling characteristics of a distributed database with the correctness guarantees offered by relational databases. The result is the Twisp Financial Ledger Database (FLDB). The Twisp FLDB is a transactionally guaranteed, secure, and highly scalable financial ledger database built for financial system use cases. It is designed to easily support massively multi-tenant workloads. Additionally, we set out to fully leverage the modern cloud. With the advent of serverless and infrastructure-as-code, the ability to define fully functioning systems with high-level code is now a reality. Twisp systems are repeatable, autoscaling, fully managed, pay-per-use, and a boon to developer productivity. > **Tip:** > > Read more about the infrastructure model in the [Infrastructure Docs](/docs/infrastructure). At its heart, Twisp is an infrastructure company. We built the FLDB because we wanted it to exist in the world. As we released this database into the wild, we found out that many companies didn't just need a ledger database—they needed an [accounting core](/docs/accounting-core). Lots of companies have re-invented a ledger and a system for accounting. Many of these systems suffer from problems of scale, complexity, and a faulty data model which can make seemingly simple tasks like calculating balances into confusing, costly chores. This is why you [shouldn't build your own ledger](/docs/introduction/no-diy-ledger). --- # Introduction What is Twisp? Source: https://www.twisp.com/docs/introduction Twisp is an accounting engine for building financial products. We provide a financial ledger database with financial primitives for engineering systems that deal with money. ... and what is a Financial Ledger? Financial ledgers are the foundation of systems that track money. Some examples: - A neobank tracks debits and credits across many accounts, with multiple financial instruments, all the while analyzing this data to surface financial insights to consumers. - A payments infrastructure company needs robust multi-tenant account servicing built on many FBOs, safely made available to many clients via API. Financial ledgers and the data derived from them are the engine of all such systems. Developing, securing, operating, and scaling these financial ledgers on a general purpose database is a difficult and time consuming proposition... which is why we built Twisp. Upon provisioning a **Twisp core ledger**, you gain access to the following features: 1. **Transaction Ledger**: Facilitates double-entry accounting, ensuring comprehensive and accurate financial recording. 2. **Journals**: Enables tracking of different currencies and monitoring activities over specific time periods. 3. **Accounts**: Represents a chart of accounts for tracking any economic activity, offering a structured view of your financial data. 4. **Transaction Codes**: Provides a method for categorizing and tracking various transaction types within the system, enhancing transactional clarity. 5. **Layered Balances**: Maintains `available`, `settled`, and `encumbrance` balances, enabling precise and timely balance reporting. This includes: - Account Hierarchies and Roll-ups: Streamlines account management and analysis. - Dimensional Balance Tracking: Provides multi-dimensional views of your financial status. - Velocity Balances: Tracks account balance changes over time. 6. **Workflows**: Automates common processes for increased efficiency, including: - Card Authorization & Settlements: Manages the lifecycle of card transactions. - ACH Transaction Processing: Handles the flow of Automated Clearing House transactions. - mRDC Check Deposits: Facilitates mobile Remote Deposit Capture operations. 7. **High-Scale API**: Serves end customers directly with a robust API, which includes: - Activity Feed: Offers real-time updates on account activities. - Enrichment: Enhances transaction data for a more detailed understanding. - Reconciliation: Matches transactions back to the ledger to ensure accuracy and eliminate customer confusion. --- # DIY Ledgers are a Problem We built a ledger database and accounting engine (so that you don't have to). Source: https://www.twisp.com/docs/introduction/no-diy-ledger The Twisp founding team has spent a cumulative total of many years working on and with financial software systems for neobanks and other fintech companies. In that time, we've learned a few things: 1. Designing, building, and managing a central accounting ledger is hard. 2. But it can appear simple, so many startups (and mature companies) decide to build their ledger in-house. 3. Most of the time, this does not go well. Accounting systems often suffer from the full range of technical debt problems common to long-lived software: data model lock-in, performance bottlenecks due to poor scaling properties, esoteric design features and programming patterns unique to one or two instrumental early engineers, etc. However, these problems be especially painful and frustrating when they apply to your internal ledger because it is both a _vital part of the business_ and also (most likely) _not a core competency_. Our friend [Matt Brown](https://www.matttbrown.com/) from Matrix Partners calls this ["undifferentiated heavy lifting"](https://www.matttbrown.com/notes/undifferentiated-heavy-lifting), and it is particularly applicable to the ledger problem: > Ledgers are a critical part of the tech and financial stack. Fundamentally, they're databases that contain debits and credits and bake in business-specific assumptions and logic to derive balances and other financial statements. However, they're also incredibly complex, requiring high uptime and performance with strict security, data integrity, audit, and regulatory requirements. Multiple business processes are built on them, so failures not only degrade UX but can cost significant sums of money. The other thing we learned is that ledgers are (mostly) a solved problem. Double-entry accounting has been around for centuries. The algorithms and architectures needed to support highly availalble, reliable, and performant ledger-type data stores have been honed for decades. There is no need to re-invent the wheel. At Twisp, we offer a simple solution to the design problem of ledgers: don't build it yourself. All of the fantastic new fintech products will need a ledger, but building internal ledgers is no longer a worthwhile investment. --- # ACH Processor Understanding how the Twisp ACH processor works Source: https://www.twisp.com/docs/processors/ach The Twisp ACH processor handles the complete lifecycle of ACH (Automated Clearing House) transactions, from origination through settlement or return. This explanation clarifies how the processor works, why it's designed this way, and how the components interact. ## What is the ACH Processor? The ACH processor is a financial infrastructure component that enables you to send and receive electronic payments through the ACH network. Unlike building custom ACH handling from scratch, the Twisp ACH processor provides production-ready workflows, file generation, webhook decisioning, and ledger integration out of the box. The processor handles two fundamental roles: - **ODFI (Originating Depository Financial Institution)**: Originate ACH transactions - **RDFI (Receiving Depository Financial Institution)**: Receive ACH transactions ## Why ACH Processing is Complex ACH might seem straightforward - just move money between accounts - but production ACH processing involves substantial complexity: **File Format Compliance:** The NACHA file format requires precise 94-character fixed-width records with specific header, batch, entry detail, and control structures. Format errors cause entire files to be rejected. **Asynchronous Settlement:** Unlike real-time payments, ACH transactions take 1-3 business days to settle. Your system must track pending, encumbered, and settled states across this timeline. **Return Handling:** Transactions can return days or even weeks after origination (up to 60 days for unauthorized returns). Returns must match back to original transactions and reverse accounting entries correctly. **Multi-Layer Balance Management:** Customer available balances must reflect encumbered funds (reserved but not sent), pending funds (sent but not confirmed), and settled funds (finalized). This prevents overdrafts and double-spending. **Webhook Decisioning:** For RDFI operations, incoming transactions require real-time business logic decisions: accept, reject, or hold for review. These decisions must execute quickly within processing timeframes. **Regulatory Compliance:** NACHA rules govern transaction types, authorization requirements, return timeframes, and file transmission security. Non-compliance risks ODFI relationship termination. The Twisp ACH processor abstracts this complexity into workflows, file operations, and ledger integration. ## Processor Architecture ### Configuration Layer ACH configuration connects all operational components: - Settlement account for fund transit - Suspense account for unidentifiable transactions - Exception account for business rule violations - Fee account for processing fees - Webhook endpoint for transaction decisioning - ODFI header information for file generation - Timezone for date/time interpretation Configuration versioning ensures immutability - files reference specific configuration versions, allowing historical analysis even after configuration updates. ### File Operations Layer File operations manage NACHA file lifecycle: **Upload:** Presigned URLs provide secure, time-limited file upload capability without storing long-lived credentials. Files upload directly to S3-compatible storage. **Processing:** File parsing validates NACHA format, extracts batch and entry details, and partitions entries for parallel processing. Each entry triggers webhook decisioning. **Generation:** File generation queries submitted transactions, groups by batch parameters, and produces NACHA-compliant files with proper control totals and trace numbers. **Download:** Presigned download URLs provide time-limited file access for transmission to financial institutions. Processing status tracking (NEW → VALIDATING → PROCESSING → COMPLETED) provides visibility into file lifecycle with statistics on entry counts and monetary totals. ### Workflow Layer Workflows orchestrate transaction state transitions and ledger accounting: **PUSH Workflow (Credits):** Sends money to receivers. CREATE encumbers customer funds, SUBMIT settles and includes in file generation. Returns reverse settled entries. **PULL Workflow (Debits):** Collects money from receivers. CREATE encumbers settlement funds, SUBMIT moves to pending, SETTLE finalizes after confirmation. The pending layer prevents premature fund availability before debit confirmation. **State Transitions:** Each workflow state transition creates ledger transactions across appropriate balance layers (encumbrance, pending, settled). This ensures double-entry accounting consistency throughout transaction lifecycle. **Execution Tracking:** Every workflow execution receives a unique ID. The execution record contains input parameters, output state, all created ledger transactions, and ACH workflow traces linking to file entries via trace numbers. ### Webhook Decisioning Layer Webhooks enable custom business logic during file processing: **Request Format:** Webhooks receive POST requests with transaction details: amount, account identifiers, entry metadata, trace number, effective date. **Response Format:** Webhook responses specify settlement instructions: which account to credit/debit, or route to suspense/exception accounts. **Timeout Handling:** Webhook timeframes are strict - typically seconds. Timeouts result in transaction failures requiring manual intervention. **Decision Routing:** - Settlement account: Normal processing, funds go to identified customer account - Suspense account: Unknown account, manual review required - Exception account: Business rule violation, transaction returned Webhook decisioning decouples payment acceptance logic from processor internals, enabling custom risk management, account validation, and compliance checking. ### Balance Layer Management Balance layers track transaction lifecycle states: **Encumbrance Layer:** Holds funds during CREATE state before transmission. Encumbered funds don't affect available balance but prevent overdrafts when transactions later settle. **Pending Layer:** PULL workflows use pending between SUBMIT and SETTLE. Pending represents funds transmitted but awaiting confirmation. Crucial for preventing premature fund availability. **Settled Layer:** Final layer for completed transactions. All accounting eventually posts to settled layer. Fees post directly to settled layer immediately. **Available Balance:** Calculated as: Settled - Pending - Encumbrance. This formula ensures customers can't spend funds that are encumbered or pending confirmation. Layer transitions via workflow states maintain double-entry balance throughout transaction lifecycle. ## Why This Design? **Separation of Concerns:** File operations, workflows, ledger accounting, and decisioning are independent components. This enables testing, scaling, and modifying each component separately. **Immutability:** Configuration versions, workflow executions, and file processing records are immutable. This provides complete audit trails required for financial operations and regulatory compliance. **Idempotency:** Workflow executions use correlation IDs. File processing matches returns via trace numbers. These identifiers enable safe retry logic and prevent duplicate processing. **Parallel Processing:** File processing partitions entries across workers. Concurrent posting to accounts (enableConcurrentPosting) allows high-throughput processing while maintaining consistency. **Webhook Flexibility:** Business logic lives in your webhook, not in the processor. This allows rapid iteration on risk rules, account validation, and compliance checks without processor changes. **Multi-Tenancy:** Configurations are tenant-scoped. Multiple organizations can operate independently on shared infrastructure with complete isolation. ## Workflow State Machines Understanding state machines clarifies transaction lifecycle: ### PUSH State Machine ``` CREATE → SUBMIT → [completed] ↓ CANCEL → REIMBURSE_FEE → [completed] SUBMIT → RETURN → REIMBURSE_FEE → [completed] ``` Credits are final upon transmission, so SUBMIT immediately settles. Returns reverse after the fact. ### PULL State Machine ``` CREATE → SUBMIT → SETTLE → [completed] ↓ CANCEL → REIMBURSE_FEE → [completed] SUBMIT → RETURN → REIMBURSE_FEE → [completed] ``` Debits require confirmation, so SETTLE finalizes after SUBMIT. Returns can occur before or after SETTLE. State machines ensure transactions follow valid progressions. Attempting invalid transitions (e.g., SUBMIT after CANCEL) fails with clear error messages. ## Return Processing Flow Returns demonstrate processor coordination: 1. **Receipt:** Return file uploads via presigned URL 2. **Parsing:** File processing extracts return entries with return codes and trace numbers 3. **Matching:** System queries original transactions via trace number indexes 4. **Execution:** RETURN workflow state executes for each matched transaction 5. **Reversal:** Ledger entries reverse from appropriate balance layer 6. **Audit:** Complete return processing history records in file and workflow execution history Automatic return processing eliminates manual ledger adjustments and ensures accounting consistency. ## Integration Points The processor integrates with other Twisp components: **Ledger Integration:** Workflows create ledger transactions via tran codes. Balance queries span encumbrance, pending, and settled layers. Concurrent posting enables high-throughput ACH processing. **Event System:** Webhook endpoints route to events system. File processing status changes emit events. Workflow state transitions trigger notifications. **Files Service:** Presigned URLs delegate upload/download to files service. Storage layer handles retention and access policies. **Workflow Engine:** Workflow executions use general workflow engine. ACH workflows are workflow templates with specific parameters and state machines. ## Design Trade-Offs **Complexity vs Flexibility:** Workflows and webhook decisioning add complexity but enable custom business logic without processor modifications. **Consistency vs Performance:** Balance layer management requires multiple ledger transactions per workflow state but ensures consistent accounting throughout transaction lifecycle. **Immutability vs Storage:** Versioning configurations and maintaining execution history increases storage but provides complete audit trails required for financial operations. **Asynchronous Processing vs Latency:** File processing partitions entries for parallel processing, adding complexity but enabling high-throughput processing of large files. These trade-offs prioritize correctness, auditability, and flexibility over simplicity. ## Production Considerations **ODFI Relationships:** You must establish banking relationships for ACH origination. ODFIs require application processes, risk assessment, and reserve accounts. Some ODFIs have minimum volume requirements or restrict certain transaction types. **File Transmission:** You're responsible for secure file transmission (typically SFTP) to/from financial institutions. The processor generates and parses files but doesn't transmit them. **Return Timeframes:** NACHA rules specify return timeframes (typically 2 business days, some codes allow 60 days). Automated return processing helps maintain compliance. **Balance Management:** Properly managing encumbrance, pending, and settled layers prevents overdrafts. Available balance calculations must account for all three layers. **Webhook Performance:** Webhook decisioning must respond within strict timeframes (typically seconds). Slow webhooks cause transaction failures. Consider caching, rate limiting, and fallback strategies. **Reconciliation:** Daily reconciliation validates file processing, ledger balances, and transaction counts. Automated reconciliation with alerting identifies issues quickly. ## Further Reading To learn ACH processing hands-on: - [Setting Up ACH Processing](/docs/tutorials/ach/setting-up-ach) - Configuration from scratch - [Your First ACH Payment](/docs/tutorials/ach/first-ach-payment) - Send a payment end-to-end For production operations: - [Processing ACH Payments](/docs/guides/processing-ach-payments) - Daily workflows - [Handling ACH Returns](/docs/guides/handling-ach-returns) - Return management - [Reconciling ACH Files](/docs/guides/reconciling-ach-files) - Daily reconciliation For technical details: - [Configuration Reference](/docs/reference/ach/configuration) - Configuration components - [File Operations Reference](/docs/reference/ach/file-operations) - File lifecycle - [ODFI Reference](/docs/reference/ach/odfi) - Originating transactions - [RDFI Reference](/docs/reference/ach/rdfi) - Receiving transactions --- # Processors Processing financial protocols and payment networks Source: https://www.twisp.com/docs/processors Processors are specialized components that handle communication with external payment networks and financial protocols. They bridge the Twisp accounting core with the outside world of financial transactions, enabling seamless integration with payment rails like ACH, wire transfers, and card networks. ## What are Processors? Processors in Twisp handle the complexities of external payment protocols, including: - **Data Format Management**: Converting between Twisp's internal formats and protocol-specific formats (e.g., NACHA for ACH) - **Automated Accounting**: Ledgering, monitoring, and reconciling the lifecycle of payments through various stages (pending, settled, returned) - **Error Handling**: Managing exceptions, returns, and corrections according to protocol rules ## Available Processors Twisp currently supports the following processors: - [**ACH**](/docs/processors/ach)\ Process ACH (Automated Clearing House) transactions including debits, credits, returns, and notifications of change for both ODFI and RDFI operators. ## Architecture Processors work in conjunction with the Twisp accounting core to provide end-to-end payment processing: 1. **Origination**: Transactions are initiated through the accounting core 2. **Processing**: The processor formats and transmits files to the appropriate network 3. **Reconciliation**: Incoming files are parsed and reconciled with ledger transactions 4. **Completion**: Final status updates are reflected in the accounting core Continue reading to learn more about specific processors and their capabilities. --- # Configuration Reference for ACH configuration within the Twisp ACH Processor Source: https://www.twisp.com/docs/reference/ach/configuration ## The Basics ACH configurations define the operational parameters for processing ACH transactions. Each configuration specifies the accounts, webhook endpoints, file header information, and timezone settings required for both originating (ODFI) and receiving (RDFI) ACH operations. Configurations in Twisp... - Connect settlement, suspense, exception, and fee accounts - Define ODFI header information for NACHA file generation - Reference webhook endpoints for transaction decisioning - Specify journal for posting all ACH transactions - Support both ODFI and RDFI operations with unified setup ## Components of ACH Configuration There are 5 primary components which define an ACH configuration: 1. **Accounts**: Four required accounts (settlement, suspense, exception, fee) that handle different transaction scenarios during ACH processing. 2. **Webhook Endpoint**: Receives decisioning requests during file processing, allowing custom business logic for transaction acceptance or rejection. 3. **ODFI Header Configuration**: Company and bank information used when generating NACHA-formatted files for transmission. 4. **Journal**: The ledger journal where all ACH transaction entries are posted. 5. **Timezone**: IANA timezone identifier for date/time operations and effective date calculations. In addition, configurations have common properties: - **Config ID**: a universally unique identifier (UUID) for the configuration. - **Version**: configurations are versioned, allowing tracking of changes over time and ensuring file processing references the correct configuration version. - **Created & Modified Timestamps**: when the configuration was created and last updated. ## Required Accounts ### Settlement Account The settlement account is the central hub for ACH fund flows. All incoming and outgoing ACH transactions post to this account during processing phases. **Characteristics:** - Normal balance type: DEBIT (typically holds positive balance from incoming credits) - `enableConcurrentPosting: true` (supports high transaction volumes) - Debited for ODFI PULL transactions, credited for ODFI PUSH transactions - Credited for RDFI inbound credits, debited for RDFI inbound debits ### Suspense Account Suspense account receives transactions where the destination account cannot be located, commonly when routing number and account number combinations don't match existing accounts. **Characteristics:** - `enableConcurrentPosting: true` (may receive high transaction volumes) - Requires manual review to determine correct account or return to originator - Funds held pending investigation or return processing ### Exception Account Exception account receives transactions that fail processing due to velocity controls, locked accounts, or other business rule violations enforced by webhook decisioning. **Characteristics:** - `enableConcurrentPosting: true` (supports parallel processing) - Funds should be returned to originator via return files - Tracks transactions rejected by business rules ### Fee Account Fee account collects ACH processing fees charged per transaction. **Characteristics:** - Normal balance type: CREDIT (accumulates fee income) - `enableConcurrentPosting: true` (supports high transaction volumes) - Credited when fees charged, debited when fees reimbursed (on returns/cancellations) ## ODFI Header Configuration ODFI header configuration contains NACHA file header information required when generating ACH files. **Components:** - **Immediate Destination**: Routing number of receiving institution (ODFI or Federal Reserve) - **Immediate Destination Name**: Name of receiving institution (max 23 characters) - **Immediate Origin**: Your Federal Tax ID or routing number (10 characters) - **Immediate Origin Name**: Your company name as it appears in ACH files (max 23 characters) These values populate the File Header Record (Record Type Code 1) in generated NACHA files. ## Webhook Endpoint The webhook endpoint receives POST requests during ACH file processing for transaction decisioning. The endpoint must be of type `ACH_PROCESSOR`. **Request Format:** Webhook receives transaction details including amount, account identifiers, and entry metadata when processing files via [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file). **Response Format:** Webhook responds with settlement instructions specifying which account to credit/debit, or directing transaction to suspense/exception accounts. **Timeout:** Webhook must respond within configured timeout period to avoid transaction failure. See [RDFI reference](/docs/reference/ach/rdfi#transaction-decisioning) for detailed webhook payload and response specifications. ## Timezone IANA timezone identifier determines date/time interpretation for: - Effective dates for ACH transactions - Settlement window calculations - Batch header dates in NACHA files - Processing cutoff time interpretation Common values: `America/New_York`, `America/Chicago`, `America/Denver`, `America/Los_Angeles`, `America/Phoenix` Use the timezone where primary ACH operations occur, typically matching ODFI timezone or business headquarters location. ## Configuration Versioning Configurations use optimistic locking with version numbers. Each update via [`Mutation.ach.updateConfiguration()`](/docs/reference/graphql/mutations#ach.update-configuration) increments the version field. File processing records reference the specific configuration version used, ensuring immutability and audit trail consistency. When files are processed via [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file), the `configVersion` field on `AchFileInfo` captures which configuration version applied, allowing historical analysis even after configuration updates. ## Configuration Operations Use GraphQL to create, update, and query ACH configurations: - [`Mutation.ach.createConfiguration()`](/docs/reference/graphql/mutations#ach.create-configuration): Create new ACH configuration. - [`Mutation.ach.updateConfiguration()`](/docs/reference/graphql/mutations#ach.update-configuration): Update existing configuration fields. - [`Query.ach.configuration()`](/docs/reference/graphql/queries#ach.configuration): Get a single configuration by ID. - [`Query.ach.configurations()`](/docs/reference/graphql/queries#ach.configurations): List all configurations with pagination. ## Further Reading To learn how to set up ACH configuration from scratch, see the tutorial on [Setting Up ACH Processing](/docs/tutorials/ach/setting-up-ach). For step-by-step configuration with real APIs, see the how-to guide on [Processing ACH Payments](/docs/guides/processing-ach-payments). For complete GraphQL type definitions, see: - [AchConfiguration](/docs/reference/graphql/types/object#ach-configuration) - [AchOdfiHeaderConfiguration](/docs/reference/graphql/types/object#ach-odfi-header-configuration) - [AchCreateConfigurationInput](/docs/reference/graphql/types/input#ach-create-configuration-input) - [AchUpdateConfigurationInput](/docs/reference/graphql/types/input#ach-update-configuration-input) --- # File Operations Reference for ACH file processing within the Twisp ACH Processor Source: https://www.twisp.com/docs/reference/ach/file-operations ## Overview ACH file operations handle the complete lifecycle of NACHA-formatted files within the Twisp platform. This reference describes how Twisp processes ACH files: from upload and validation through parallel processing and generation. Understanding these internals helps you optimize file processing, debug issues, and integrate effectively with the ACH system. The Twisp ACH file processor: - Validates NACHA file format compliance with detailed error reporting - Partitions files for parallel transaction processing - Generates NACHA-compliant files from workflow transactions - Provides presigned URLs for secure file upload and download with expiration - Tracks file processing through multiple states with real-time statistics - Maintains complete file processing history and audit trails - Stores files in S3 with retention policies and versioning ## Components of File Operations There are 4 primary components in ACH file processing: 1. **File Upload**: Create presigned URLs for secure file uploads, supporting ACH and bulk GraphQL variable files. 2. **File Processing**: Parse NACHA files, validate format compliance, extract transactions, and trigger webhook decisioning for each entry. 3. **File Generation**: Create NACHA-formatted files from submitted transactions, ready for transmission to financial institutions. 4. **File Download**: Generate presigned URLs for secure file downloads with automatic expiration. In addition, file records maintain: - **File ID**: UUID derived from file key for tracking specific file processing instances. - **Processing Status**: Current state (NEW, VALIDATING, PROCESSING, COMPLETED, ERROR, etc.). - **Processing Statistics**: Entry counts, credit/debit totals, unprocessed entry counts. - **Configuration Version**: Which ACH configuration version was used for processing. - **History**: Complete audit trail of all status changes and statistic updates. ## File Upload File upload uses a two-step process: first create a presigned URL, then upload file content via HTTP PUT. **Upload Types:** - `ACH`: NACHA-formatted text files for transaction processing - `BULK_GRAPHQL_VARIABLES`: JSON arrays for bulk GraphQL query execution **File Requirements for ACH:** - Content type: `text/plain` - Format: NACHA (National Automated Clearing House Association) standard - Character encoding: ASCII - Line endings: CRLF or LF - Record length: 94 characters per line Presigned upload URLs expire after a configured period. If expiration occurs before upload completes, create a new upload request. ## File Processing File processing parses uploaded NACHA files, validates format compliance, and extracts transaction details for decisioning. **File Types:** RDFI (Receiving): - `RDFI`: Incoming transactions to receive (credits and debits) - `RDFI_RETURN`: Return files from ODFI for transactions you originated - `RDFI_NOC`: Notification of Change files ODFI (Originating): - `ODFI`: Outgoing file with both PUSH and PULL transactions - `ODFI_PULL_ONLY`: Outgoing file with only PULL (debit) transactions - `ODFI_PUSH_ONLY`: Outgoing file with only PUSH (credit) transactions - `ODFI_RETURN`: Outgoing return file for RDFI transactions you received - `ODFI_PREPROCESS_RETURN`: Pre-process return file before final generation - `ODFI_PROCESSED`: Already processed ODFI file for reconciliation **Processing Phases:** 1. **Validation**: Verify NACHA format compliance, record structure, control totals 2. **Partitioning**: Split file for parallel transaction processing 3. **Webhook Decisioning**: Send each transaction to configured webhook endpoint 4. **Settlement**: Apply settlement instructions from webhook responses 5. **Completion**: Mark all transactions settled or queued for returns **Preprocessing Options:** When using `ODFI_PREPROCESS_RETURN` file type, specify output file keys for filtered results: - `preprocessedFileKey`: Entries passing preprocessing rules - `preprocessedExcludedFileKey`: Entries failing preprocessing rules ## File Generation File generation creates NACHA-formatted files from submitted workflow transactions. **Generation Options:** - `generateEmpty: true`: Always create file, even without transactions (empty NACHA with headers) - `generateEmpty: false`: Only create file if transactions exist (returns `generated: false` if none) **File Type Selection:** Use separate file types when: - ODFI requires distinct files for credits vs debits - Different transmission schedules for transaction types - Risk management requires transaction type isolation - Separate approval workflows needed Use combined `ODFI` file type when: - Processing both credits and debits together - Single transmission window - Financial institution accepts mixed transaction files Generated files are NACHA-compliant and ready for transmission via SFTP, FTPS, or other secure transfer methods. ## File Download File download provides presigned URLs for retrieving generated or processed files. **Download Process:** 1. Request download URL via GraphQL mutation 2. Receive presigned URL with expiration timestamp 3. Download file content via HTTP GET before expiration Presigned download URLs expire after configured period. If URL expires, create new download request with same file key. ## File Lifecycle Deep Dive Understanding how Twisp processes ACH files helps you optimize integration, debug issues, and monitor progress effectively. ### Processing Phases File processing progresses through multiple phases, each with specific responsibilities and state transitions. #### Phase 1: Upload (NEW → UPLOADED) **NEW State:** - File record created via [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file) - File ID generated from file key - Initial `AchFileInfo` record stored with configuration version - Processing status set to `NEW` **UPLOADED State:** - File content uploaded to S3 via presigned URL from [`Mutation.files.createUpload()`](/docs/reference/graphql/mutations#files.create-upload) - System detects uploaded file and transitions to `UPLOADED` - File queued for validation - Processing statistics initialized to zero **Transition Trigger:** S3 upload completion detected by file processor #### Phase 2: Validation (UPLOADED → VALIDATING → PARTITIONING) **VALIDATING State:** - Parser reads NACHA file structure - Validates file header record (Record Type 1) - Validates batch header records (Record Type 5) - Validates entry detail records (Record Type 6) - Validates addenda records (Record Type 7) - Validates control records (Record Types 8, 9) - Checks hash totals and entry counts - Verifies record length (94 characters) **Validation Checks:** - File header immediate destination/origin format - Batch header service class codes - Entry detail transaction codes - Routing number check digit validation - Control total reconciliation - Record sequence validation **On Success:** Transitions to `PARTITIONING` **On Failure:** Transitions to `INVALID` with detailed error in `processingDetail` #### Phase 3: Partitioning (PARTITIONING → PROCESSING) **PARTITIONING State:** - File split into partitions for parallel processing - Each partition contains subset of entry detail records - Partition size optimized for concurrent webhook processing - ACH workflow trace records created for each entry - Trace numbers extracted and indexed for return matching **Partitioning Strategy:** - Default: 100 entries per partition - Configurable based on expected webhook latency - Partitions processed independently by worker pool - Enables horizontal scaling of webhook processing **Transition Trigger:** All partitions created and queued #### Phase 4: Webhook Processing (PROCESSING → PROCESSED) **PROCESSING State (RDFI only):** - Each entry triggers webhook to configured endpoint - Webhooks sent in parallel across partitions - Responses collected: `SETTLE`, `RETURN`, or `RETRY` - Statistics updated: `numEntriesUnprocessed` decrements - Retry logic for failed webhooks (exponential backoff) **ODFI Files:** - ODFI files skip webhook processing - Transition directly to `PROCESSED` after partitioning **Monitoring:** Query `processingStatistics.numEntriesUnprocessed` via [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file) to track progress: ```graphql query MonitorFileProcessing { ach { file(fileKey: "incoming-20251114.ach", configId: "config-001") { processingStatus processingStatistics { numEntriesUnprocessed totalCreditAmount totalDebitAmount } } } } ``` **Transition Trigger:** `numEntriesUnprocessed` reaches zero #### Phase 5: Settlement (PROCESSED → COMPLETED) **PROCESSED State:** - All webhooks sent and responses received - Transactions awaiting settlement based on effective dates - For RDFI: Settlement occurs when workflow SETTLE state executes - For ODFI: Settlement occurs immediately after file generation **COMPLETED State:** - All transactions settled or queued for returns - File processing complete - Final statistics recorded - File available for download **Transition Trigger:** All transactions reach terminal state (settled or returned) ### State Transition Diagram ``` ┌─────────┐ │ NEW │ File record created └────┬────┘ │ File uploaded ↓ ┌─────────┐ │UPLOADED │ File content in S3 └────┬────┘ │ Validation starts ↓ ┌──────────┐ Validation fails ┌─────────┐ │VALIDATING├────────────────────────→│ INVALID │ Terminal └────┬─────┘ └─────────┘ │ Validation succeeds ↓ ┌─────────────┐ Processing error ┌────────┐ │PARTITIONING ├──────────────────────→│ ERROR │ Terminal └──────┬──────┘ └────────┘ │ Partitions created ↓ ┌────────────┐ Manual abort ┌─────────┐ │ PROCESSING ├───────────────────────→│ ABORTED │ Terminal └──────┬─────┘ └─────────┘ │ All webhooks sent ↓ ┌───────────┐ │ PROCESSED │ Awaiting settlements └─────┬─────┘ │ All settled/returned ↓ ┌───────────┐ │ COMPLETED │ Terminal └───────────┘ ``` ### Processing Status Reference | Status | Description | Next States | Typical Duration | |--------|-------------|-------------|------------------| | `NEW` | File record created | `UPLOADED`, `ERROR` | < 1 second | | `UPLOADED` | File in S3, queued | `VALIDATING` | < 5 seconds | | `VALIDATING` | Format validation | `PARTITIONING`, `INVALID` | 5-30 seconds | | `PARTITIONING` | Creating partitions | `PROCESSING`, `ERROR` | 5-15 seconds | | `PROCESSING` | Sending webhooks | `PROCESSED`, `ABORTED` | Minutes to hours* | | `PROCESSED` | Webhooks complete | `COMPLETED` | Hours to days** | | `COMPLETED` | Final state | None | Permanent | | `INVALID` | Validation failed | None | Permanent | | `ERROR` | Processing error | None | Permanent | | `ABORTED` | Manually stopped | None | Permanent | \* Depends on webhook response time and retry logic \** Depends on transaction effective dates and settlement timing ## Processing Statistics Processing statistics track entry counts and monetary totals during file processing. **Statistics Fields:** - `numEntriesUnprocessed`: Entries awaiting webhook responses - `totalCreditAmount`: Sum of all credit transactions (in cents) - `totalDebitAmount`: Sum of all debit transactions (in cents) Statistics update throughout processing lifecycle, providing real-time visibility into file processing progress. ## File Format Validation Twisp validates NACHA file format compliance during the `VALIDATING` phase. Understanding validation helps you debug file issues and ensure compliance. ### What Twisp Validates **File Structure:** - Record length: Every line must be exactly 94 characters - Record types: Must be 1, 5, 6, 7, 8, or 9 - Record sequence: File Header → Batch(es) → File Control - Line endings: CRLF or LF accepted - Character encoding: ASCII only **File Header Record (Type 1):** - Immediate destination: 10 characters (routing number or " ") - Immediate origin: 10 characters (Tax ID or routing number) - File creation date/time: Valid YYMMDD and HHMM - File ID modifier: Single character A-Z or 0-9 **Batch Header Record (Type 5):** - Service class code: 200 (mixed), 220 (credits), 225 (debits) - Company identification: 10 characters - Standard entry class code: Valid SEC code (PPD, CCD, WEB, etc.) - Effective entry date: Valid YYMMDD format - Originator status code: 0, 1, or 2 - ODFI identification: 8-digit routing number **Entry Detail Record (Type 6):** - Transaction code: Valid code (22, 23, 24, 27, 28, 29, 32, 33, 34, 37, 38, 39) - Receiving DFI identification: 8 digits - Check digit: Matches routing number check digit algorithm - DFI account number: 1-17 characters - Amount: 10-digit number (cents) - Trace number: 15 digits (ODFI routing + sequence) **Batch Control Record (Type 8):** - Entry count matches actual entries in batch - Entry hash matches sum of RDFI routing numbers (rightmost 10 digits) - Total debit amount matches sum of debit entries - Total credit amount matches sum of credit entries **File Control Record (Type 9):** - Batch count matches actual batches - Block count correct (records / 10, rounded up) - Entry and addenda count matches total - Entry hash matches sum of all RDFI routing numbers - Total debit/credit amounts match sums across all batches ### Common Validation Errors **Record Length Errors:** ``` Error: "Line 15: Record length is 93, expected 94" Fix: Ensure all lines are exactly 94 characters (pad with spaces if needed) ``` **Check Digit Errors:** ``` Error: "Line 42: Check digit 7 does not match calculated value 3" Fix: Recalculate check digit using ABA routing number algorithm ``` **Control Total Mismatch:** ``` Error: "Batch 1: Entry count 150 does not match control record value 148" Fix: Verify all entries included in batch, regenerate control record ``` **Invalid Transaction Code:** ``` Error: "Line 67: Invalid transaction code 21" Fix: Use valid codes (22-29 for checking, 32-39 for savings) ``` **Entry Hash Mismatch:** ``` Error: "File Control: Entry hash 1234567890 does not match calculated 1234567891" Fix: Sum first 8 digits of all RDFI routing numbers, take rightmost 10 digits ``` ### Debugging Validation Failures When a file fails validation (status `INVALID`), check `processingDetail` for error information: ```graphql query GetValidationError { ach { file(fileKey: "problematic-file.ach", configId: "config-001") { fileId processingStatus processingDetail modified } } } ``` **Example `processingDetail` values:** - `"Line 42: Record length is 93, expected 94"` - `"Batch 1: Entry count mismatch (expected 150, got 148)"` - `"File Control: Total debit amount mismatch"` ### Validation Best Practices **Before Uploading:** - Use NACHA file validation tools - Verify record lengths (94 characters exactly) - Check control totals match entry counts - Validate routing number check digits - Ensure valid transaction codes for account types **After Validation Errors:** - Read `processingDetail` for specific line/field errors - Fix source data or file generation logic - Re-upload corrected file with new file key - Consider automated pre-validation before Twisp upload ## Parallel Processing Architecture Twisp partitions large ACH files for concurrent webhook processing, enabling high-throughput file processing. ### How Partitioning Works **Partition Creation (PARTITIONING state):** 1. File parsed and validated 2. Entry detail records extracted 3. Entries divided into partitions (default 100 per partition) 4. Each partition assigned to worker in pool 5. Workers process partitions concurrently **Partition Processing:** - Each worker processes its partition independently - Webhooks sent in parallel across all workers - No cross-partition dependencies - Failures in one partition don't affect others **Concurrency Model:** ``` File: 1,000 entries ├── Partition 1: Entries 1-100 → Worker 1 ├── Partition 2: Entries 101-200 → Worker 2 ├── Partition 3: Entries 201-300 → Worker 3 ├── ... └── Partition 10: Entries 901-1000 → Worker 10 Each worker sends 100 webhooks in parallel Total: 1,000 webhooks sent concurrently across 10 workers ``` ### Performance Characteristics **Throughput:** - Small files (< 100 entries): 30-60 seconds total - Medium files (100-1,000 entries): 2-5 minutes with fast webhooks - Large files (1,000-10,000 entries): 10-30 minutes with fast webhooks - Very large files (> 10,000 entries): Linear scaling with partition count **Bottlenecks:** - Webhook response time (primary factor) - Webhook endpoint throughput - Database transaction commit latency - Network bandwidth for webhook traffic **Optimization Tips:** - Optimize webhook endpoint for < 100ms response time - Use async processing in webhook handler - Enable connection pooling for database access - Consider horizontal scaling of webhook endpoint - Monitor `numEntriesUnprocessed` to track progress ### Monitoring Parallel Processing Track processing progress in real-time via [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file): ```graphql query MonitorProcessing { ach { file(fileKey: "large-file.ach", configId: "config-001") { processingStatus processingStatistics { numEntriesUnprocessed totalCreditAmount totalDebitAmount } modified } } } ``` Poll every 5-10 seconds during `PROCESSING` state to track `numEntriesUnprocessed` countdown. ## Storage and Retention Twisp stores ACH files in AWS S3 with security, retention, and versioning policies. ### File Storage Location **S3 Bucket Structure:** ``` twisp-ach-files-{region}/ ├── uploads/ │ ├── {tenantId}/ │ │ └── {fileKey} ├── generated/ │ ├── {tenantId}/ │ │ └── {fileKey} └── archived/ ├── {tenantId}/ └── {fileKey} ``` **Storage Classes:** - Uploads: Standard S3 (frequent access) - Generated files: Standard S3 (frequent access for 30 days) - Archived files: Glacier after 90 days (compliance retention) ### Retention Policies **Upload Files:** - Retained for 90 days after upload - Archived to Glacier for 7 years (NACHA compliance) - Accessible via file key throughout retention period **Generated Files:** - Retained for 90 days in Standard S3 - Archived to Glacier for 7 years - Download URLs valid for 15 minutes (renewable) **File Metadata:** - `AchFileInfo` records retained permanently - Processing history retained for audit trail - Statistics snapshots retained at each version - Enables compliance reporting and reconciliation ### Versioning **File Versioning:** - S3 versioning enabled on ACH file buckets - Protects against accidental deletion - Enables recovery of overwritten files - Version history retained for full retention period **Metadata Versioning:** - `AchFileInfo.version` increments on each update - `AchFileInfo.history` connection provides all versions - Each version captures processing state snapshot - Enables point-in-time analysis ### Security **Encryption:** - S3 server-side encryption (SSE-S3) enabled - Files encrypted at rest - Presigned URLs use HTTPS only - API access requires authentication **Access Control:** - Tenant-isolated S3 paths - IAM roles restrict cross-tenant access - Presigned URLs scoped to specific file key - Time-limited URLs (15-minute expiration) ## Upload and Download Mechanics Twisp uses presigned URLs for secure, direct S3 access without exposing AWS credentials. ### Upload Process **Step 1: Create Upload URL** Call [`Mutation.files.createUpload()`](/docs/reference/graphql/mutations#files.create-upload) to get presigned URL: ```graphql mutation CreateUpload { files { createUpload( input: { key: "incoming-20251114.ach" uploadType: ACH contentType: "text/plain" } ) { uploadURL } } } ``` **Response:** ```json { "files": { "createUpload": { "uploadURL": "https://s3.amazonaws.com/twisp-ach-files/.../incoming-20251114.ach?X-Amz-..." } } } ``` **Step 2: Upload File Content** Use HTTP PUT to upload file to presigned URL: ```bash curl -T incoming-20251114.ach \ -H "Content-Type: text/plain" \ -XPUT '' ``` **Important:** - URL expires after 15 minutes - Content-Type must match specified type - Maximum file size: 100 MB - Upload must complete before expiration **Step 3: Process File** After upload completes, start processing with [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file): ```graphql mutation ProcessFile { ach { processFile( input: { configId: "config-001" fileKey: "incoming-20251114.ach" fileType: RDFI } ) { fileId } } } ``` ### Download Process **Step 1: Generate Download URL** Call [`Mutation.files.createDownload()`](/docs/reference/graphql/mutations#files.create-download) to get presigned URL: ```graphql mutation CreateDownload { files { createDownload( key: "outgoing-20251114.ach" ) { downloadURL } } } ``` **Step 2: Download File Content** Use HTTP GET to download file from presigned URL: ```bash curl '' -o outgoing-20251114.ach ``` **Important:** - URL expires after 15 minutes - If expired, generate new download URL - No authentication required (URL contains signature) - Download bandwidth not rate-limited ### Presigned URL Security Model **How It Works:** 1. Twisp generates presigned URL with AWS STS 2. URL contains temporary credentials in query parameters 3. AWS validates signature on access 4. Access granted without exposing permanent credentials **Security Properties:** - Time-limited access (15-minute expiration) - Scoped to specific file key - Cannot be used to access other files - Revoked automatically on expiration - HTTPS only (signatures invalid over HTTP) **Best Practices:** - Generate URLs just before use - Don't store URLs long-term - Don't share URLs externally - Monitor for expired URL errors - Implement retry logic with fresh URLs ### Large File Handling **Upload Optimization:** - Use streaming uploads for large files - Monitor upload progress with Content-Length header - Implement retry logic for network failures - Consider multipart upload for > 50 MB files **Download Optimization:** - Use Range requests for partial downloads - Implement resume capability for interrupted downloads - Stream downloads rather than loading into memory - Consider parallel chunk downloads for very large files ## File History File records maintain complete history of status changes and statistic updates via the `history` connection field queried through [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file). Each version captures: - Processing status at that point in time - Processing detail messages - Statistics snapshot - Modification timestamp - Version number History enables: - Complete audit trail of file processing - Point-in-time analysis of processing state - Debugging processing issues - Compliance and regulatory reporting ## File Queries Query files by status using the `PROCESSING_STATUS` index, which requires both `processingStatus` and `configId` filter values. **Supported Filters:** - `configId`: Filter to specific ACH configuration - `processingStatus`: Filter by current processing state - `created`: Filter by file creation timestamp range Results support pagination via standard connection cursors. ## File Operations Use GraphQL for all file operations: **Upload and Processing:** - [`Mutation.files.createUpload()`](/docs/reference/graphql/mutations#files.create-upload): Create presigned upload URL - [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file): Start file processing - [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file): Get file information by key or ID - [`Query.ach.files()`](/docs/reference/graphql/queries#ach.files): Query files with status filters **Generation and Download:** - [`Mutation.ach.generateFile()`](/docs/reference/graphql/mutations#ach.generate-file): Generate NACHA file - [`Mutation.files.createDownload()`](/docs/reference/graphql/mutations#files.create-download): Create presigned download URL - [`Query.files.list()`](/docs/reference/graphql/queries#files.list): List files by key prefix ## Further Reading To learn file operations from scratch, see the tutorial on [Setting Up ACH Processing](/docs/tutorials/ach/setting-up-ach). For practical file processing workflows, see the how-to guide on [Reconciling ACH Files](/docs/guides/reconciling-ach-files). For complete GraphQL type definitions, see: - [AchFileInfo](/docs/reference/graphql/types/object#ach-file-info) - [AchProcessingStatistics](/docs/reference/graphql/types/object#ach-processing-statistics) - [AchFileType](/docs/reference/graphql/types/enum#ach-file-type) - [AchFileProcessingStatus](/docs/reference/graphql/types/enum#ach-file-processing-status) - [Upload](/docs/reference/graphql/types/object#upload) - [Download](/docs/reference/graphql/types/object#download) --- # ACH Reference The Twisp ACH processor handles configuration, file operations, ODFI origination, and RDFI receiving operations. Source: https://www.twisp.com/docs/reference/ach ## ACH Resources See each of the sub-pages in this reference for details about the corresponding ACH resource. Each resource is accessible via the [GraphQL API](/docs/reference/graphql). - [Configuration](/docs/reference/ach/configuration): ACH configuration defines accounts, webhooks, ODFI headers, and processing parameters. - [File Operations](/docs/reference/ach/file-operations): Upload, process, generate, and download NACHA-formatted ACH files. - [ODFI](/docs/reference/ach/odfi): Originate ACH transactions via PUSH (credit) and PULL (debit) workflows. - [RDFI](/docs/reference/ach/rdfi): Receive and process incoming ACH transactions with webhook decisioning. ## Key Concepts of the Twisp ACH Processor _This set of concepts summarizes the key behavior of the Twisp ACH processor._ - **Configuration** defines the operational parameters for ACH processing. - Each configuration connects settlement, suspense, exception, and fee accounts. - Configurations reference webhook endpoints for transaction decisioning. - ODFI header configuration specifies company and bank information for NACHA files. - Configurations support both ODFI (originating) and RDFI (receiving) operations. - Configuration versions are tracked, ensuring file processing references the correct parameters. - **File Operations** manage the complete lifecycle of NACHA-formatted files. - Upload operations create presigned URLs for secure file uploads. - File processing parses NACHA files, validates format, and extracts transactions. - File generation creates NACHA-compliant files from submitted workflow transactions. - Download operations provide presigned URLs for retrieving generated files. - File processing status progresses through states (NEW, VALIDATING, PROCESSING, COMPLETED). - Processing statistics track entry counts and monetary totals in real-time. - **ODFI (Originating Depository Financial Institution)** operations originate ACH transactions. - PUSH workflows send credits (payroll, vendor payments, refunds). - PULL workflows collect debits (bill payments, subscriptions, loan payments). - Workflows manage funds through encumbrance, pending, and settled balance layers. - PUSH workflows settle immediately upon SUBMIT since credits are final. - PULL workflows separate SUBMIT (transmission) from SETTLE (confirmation). - Return processing automatically matches returns to original transactions via trace numbers. - All workflow executions maintain complete audit trails via execution history. - **RDFI (Receiving Depository Financial Institution)** operations receive ACH transactions. - Inbound file processing extracts transactions and triggers webhook decisioning. - Webhooks receive transaction details and respond with settlement instructions. - Transactions route to settlement, suspense, or exception accounts based on webhook responses. - RDFI workflows support both CREATE (initial acceptance) and SETTLE (final settlement) states. - Return file generation creates NACHA files for rejected transactions. - NOC (Notification of Change) processing updates account information automatically. - **Workflows** execute state transitions for ACH transactions. - Each workflow execution receives a unique execution ID for tracking. - Workflow states (CREATE, SUBMIT, SETTLE, RETURN, CANCEL, REIMBURSE_FEE) define transaction lifecycle. - Ledger transactions are created automatically based on workflow state transitions. - ACH workflow traces link executions to file entries via trace numbers. - Workflow history provides complete audit trail of all state transitions. - **Balance Layers** track transaction lifecycle across multiple states. - Encumbrance layer holds funds during CREATE state before submission. - Pending layer tracks PULL transactions between SUBMIT and SETTLE. - Settled layer contains finalized transactions. - Available balance calculation: Settled - Pending - Encumbrance. - Layer transitions ensure funds are properly accounted at each stage. - **Webhook Decisioning** enables custom business logic for transaction processing. - Webhooks receive POST requests with transaction details during file processing. - Webhook responses specify which account to credit/debit for each transaction. - Transactions can be routed to settlement, suspense, or exception accounts. - Webhook timeouts result in transaction failure requiring manual intervention. - Webhook decisions determine whether transactions settle or return. - **Return Processing** handles rejected transactions from financial institutions. - ODFI returns are received for transactions you originated. - RDFI returns are generated for transactions you received and must reject. - Returns automatically match to original transactions via trace numbers. - Return processing executes RETURN workflow state, reversing ledger entries. - Return codes (R01-R99) indicate rejection reasons per NACHA standards. - Return files must be transmitted within NACHA-specified timeframes. - **File Types** specify the purpose and direction of ACH files. - RDFI types: RDFI (incoming), RDFI_RETURN (returns to send), RDFI_NOC (notifications). - ODFI types: ODFI (mixed), ODFI_PUSH_ONLY (credits), ODFI_PULL_ONLY (debits), ODFI_RETURN (returns received). - File types control which transactions are included during generation. - Preprocessing file types enable filtering before final file generation. - **Reconciliation** validates file processing and ledger accuracy. - File statistics provide entry counts and monetary totals for validation. - Processing status indicates completion or errors requiring investigation. - Balance reconciliation ensures settlement accounts reflect all ACH activity. - Suspense and exception accounts should be cleared regularly via returns or transfers. - File history enables point-in-time analysis of processing state. ## Further Reading To learn ACH processing from scratch, see: - [Setting Up ACH Processing](/docs/tutorials/ach/setting-up-ach) - Complete configuration tutorial - [Your First ACH Payment](/docs/tutorials/ach/first-ach-payment) - Send a payment end-to-end For practical production workflows, see: - [Processing ACH Payments](/docs/guides/processing-ach-payments) - Daily payment operations - [Handling ACH Returns](/docs/guides/handling-ach-returns) - Return management - [Reconciling ACH Files](/docs/guides/reconciling-ach-files) - Daily reconciliation For conceptual understanding, see: - [ACH Processor](/docs/processors/ach) - How the ACH processor works For complete GraphQL type definitions, see: - [AchConfiguration](/docs/reference/graphql/types/object#ach-configuration) - [AchFileInfo](/docs/reference/graphql/types/object#ach-file-info) - [WorkflowExecution](/docs/reference/graphql/types/object#workflow-execution) --- # ODFI Reference for ODFI operations within the Twisp ACH Processor Source: https://www.twisp.com/docs/reference/ach/odfi ## Overview An ODFI (Originating Depository Financial Institution) originates ACH transactions on behalf of an originator. When operating as an ODFI, the Twisp ACH processor handles transaction lifecycle management, ledger accounting across multiple balance layers, NACHA file generation, and return processing for outgoing ACH payments. The ACH ODFI processor enables you to: - Originate ACH credit (PUSH) and debit (PULL) transactions via workflows - Manage funds through encumbrance, pending, and settled balance layers - Generate NACHA-compliant files for transmission to financial institutions - Track transaction lifecycle from creation through settlement or return - Process returns from RDFIs with automatic ledger reversals - Maintain complete audit trails via workflow execution history ## Getting Started ### Prerequisites Before originating ACH transactions, you need: 1. **ACH Configuration** - Created via [`Mutation.ach.createConfiguration()`](/docs/reference/graphql/mutations#ach.create-configuration) 2. **Required Accounts** - Settlement, suspense, exception, and fee accounts 3. **Journal** - For posting all ACH transactions 4. **Customer Accounts** - Accounts to debit (PUSH) or credit (PULL) ### Quick Start Example ```graphql # 1. Create required accounts mutation CreateAccounts { # Settlement account - where funds transit during ACH processing settlement: createAccount( input: { accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" code: "settlement.ach" name: "ACH Settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } # Suspense account - for transactions to unknown accounts suspense: createAccount( input: { accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" code: "suspense.ach" name: "ACH Suspense" config: { enableConcurrentPosting: true } } ) { accountId } # Exception account - for failed transactions exception: createAccount( input: { accountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" code: "exception.ach" name: "ACH Exception" config: { enableConcurrentPosting: true } } ) { accountId } # Fee account - for ACH processing fees fee: createAccount( input: { accountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" code: "fee.ach" name: "ACH Fee Income" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId } # Customer account - for testing customer: createAccount( input: { accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" code: "customer.001" name: "Customer Account" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } } # 2. Create a journal for ACH transactions mutation CreateJournal { createJournal( input: { journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" name: "ACH Processing Journal" status: ACTIVE } ) { journalId } } # 3. Create a webhook endpoint (Note: webhook endpoint not used in ODFI-only use cases) mutation CreateACHWebhookProcessor { events { createEndpoint( input: { endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" status: ENABLED endpointType: ACH_PROCESSOR url: "https://webhook.site/ach-testing" subscription: [] description: "ACH webhook processor" } ) { endpointId } } } # 4. Create ACH configuration mutation CreateACHConfig { ach { createConfiguration( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" exceptionAccountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" odfiHeaderConfiguration: { immediateDestination: "021000021" immediateDestinationName: "Federal Reserve Bank" immediateOrigin: "1234567890" immediateOriginName: "Your Company Name" } timeZone: "America/New_York" } ) { configId version } } } ``` ## ODFI Workflows ### Workflow Types There are 2 primary workflow types for originating ACH transactions: 1. **ACH PUSH (Credits)**: Send funds to receivers. Workflow ID `934498b5-b4f1-46c4-ad79-868939dc39e8`. Used for payroll, vendor payments, refunds. 2. **ACH PULL (Debits)**: Collect funds from receivers. Workflow ID `064e3b76-6072-451e-aace-5a7be3704ee2`. Used for bill payments, subscriptions, loan payments. Both workflows accept the same parameters but follow different balance layer progression based on settlement timing requirements. ### PUSH Workflow (ACH Credits) The PUSH workflow originates credit transactions that send funds to receivers. When you execute a PUSH workflow via [`Mutation.workflow.execute()`](/docs/reference/graphql/mutations#workflow.execute), funds are immediately encumbered on the customer's account and settled upon submission. #### CREATE State The CREATE state reserves funds and charges processing fees: ```graphql mutation CreatePushTransaction { workflow { execute( input: { executionId:"daf20572-c1b1-11f0-8b14-069b540ea27c" code: "ACH_PUSH" task: "CREATE" params: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" amount: "1500.00" routingNumber: "026009593", accountNumber: "12345678901234567", accountType: "checking", individualName: "Clark Kent", entryDescription: "XFER", effective:"2025-11-14" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" feeAmount: "0.25" correlationId: "payroll-batch-001" metadata: { employeeId: "emp-123" payrollPeriod: "2025-11-01" } entryMetadata: { companyName: "ACME Corp" companyEntryDescription: "PAYROLL" individualName: "John Doe" standardEntryClassCode: "PPD" dfiAccountNumber: "123456789" identificationNumber: "emp-123" rdfiIdentification: "021000021" } } } ) { executionId output { state } } } } ``` **Ledger Entries Created:** Encumbrance layer: ``` DR Customer Account (Encumbrance) $1,500.00 CR Settlement Account (Encumbrance) $1,500.00 ``` Settled layer (fees): ``` DR Customer Account (Settled) $0.25 CR Fee Account (Settled) $0.25 ``` #### SUBMIT State The SUBMIT state finalizes the transaction for file generation: ```graphql mutation SubmitPushTransaction { workflow { execute( input: { executionId:"daf20572-c1b1-11f0-8b14-069b540ea27c" code: "ACH_PUSH" task: "SUBMIT" params: {} } ) { executionId output { state } } } } ``` **Ledger Entries Created:** Reverse encumbrance: ``` DR Customer Account (Encumbrance) -$1,500.00 CR Settlement Account (Encumbrance) -$1,500.00 ``` Post to settled: ``` DR Customer Account (Settled) $1,500.00 CR Settlement Account (Settled) $1,500.00 ``` The transaction is now queued for inclusion in the next file generated via [`Mutation.ach.generateFile()`](/docs/reference/graphql/mutations#ach.generate-file). #### CANCEL State Cancel a transaction before file generation: ```graphql mutation CancelPushTransaction { workflow { execute( input: { executionId:"daf20572-c1b1-11f0-8b14-069b540ea27c" code: "ACH_PUSH" task: "CANCEL" params: {} } ) { executionId output { state } } } } ``` Reverses the CREATE encumbrance. Follow with REIMBURSE_FEE to refund processing fees. ### PULL Workflow (ACH Debits) The PULL workflow originates debit transactions that collect funds from receivers. PULL workflows have an additional SETTLE state between SUBMIT and final settlement. #### CREATE State The CREATE state reserves funds on the settlement account: ```graphql mutation CreatePullTransaction { workflow { execute( input: { executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" code: "ACH_PULL" task: "CREATE" params: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" routingNumber: "026009593", accountNumber: "12345678901234567", accountType: "checking", individualName: "Clark Kent", entryDescription: "XFER", effective:"2025-11-14" amount: "250.00" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" feeAmount: "0.25" correlationId: "billing-batch-001" metadata: { customerId: "cust-456" invoiceNumber: "INV-2025-001" } entryMetadata: { companyName: "ACME Corp" companyEntryDescription: "INVOICE" individualName: "Jane Smith" standardEntryClassCode: "WEB" dfiAccountNumber: "987654321" identificationNumber: "inv-001" rdfiIdentification: "021000021" } } } ) { executionId output { state } } } } ``` **Ledger Entries Created:** Encumbrance layer (inverse of PUSH): ``` DR Settlement Account (Encumbrance) $250.00 CR Customer Account (Encumbrance) $250.00 ``` Settled layer (fees): ``` DR Customer Account (Settled) $0.25 CR Fee Account (Settled) $0.25 ``` #### SUBMIT State The SUBMIT state moves funds to pending layer for file generation: ```graphql mutation SubmitPullTransaction { workflow { execute( input: { executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" code: "ACH_PULL" task: "SUBMIT" params: {} } ) { executionId output { state } } } } ``` **Ledger Entries Created:** Reverse encumbrance: ``` DR Settlement Account (Encumbrance) -$250.00 CR Customer Account (Encumbrance) -$250.00 ``` Post to pending: ``` DR Settlement Account (Pending) $250.00 CR Customer Account (Pending) $250.00 ``` The transaction remains in pending layer until SETTLE execution. #### SETTLE State The SETTLE state finalizes the transaction after collection confirmation: ```graphql mutation SettlePullTransaction { workflow { execute( input: { executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" code: "ACH_PULL" task: "SETTLE" params: {} } ) { executionId output { state } } } } ``` **Ledger Entries Created:** Reverse pending: ``` DR Settlement Account (Pending) -$250.00 CR Customer Account (Pending) -$250.00 ``` Post to settled: ``` DR Settlement Account (Settled) $250.00 CR Customer Account (Settled) $250.00 ``` ### Monitoring Workflow Execution Query workflow execution details using [`Query.workflow.execution()`](/docs/reference/graphql/queries#workflow.execution): ```graphql query GetWorkflowExecution { workflow { execution( executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" ) { workflowId executionId task params output { state } activities { action entityType entityId entity { ... on Transaction { transactionId effective description entries(first:4) { nodes { accountId layer amount { units currency } } } } ... on AchWorkflowTrace { traceNumber fileId configId } } } created modified version } } } ``` ## Workflow Parameters All ODFI workflows accept common parameters: - **Account ID**: Customer account to debit (PUSH) or credit (PULL) - **Settlement Account ID**: Central account for fund transit during processing - **Journal ID**: Ledger journal for posting all transactions - **Amount**: Transaction amount in decimal format (e.g., "1500.00") - **Effective Date**: Settlement date in YYYY-MM-DD format - **Fee Account ID** (optional): Account for fee income (default: zero UUID) - **Fee Amount** (optional): Processing fee in decimal format (default: "0") - **Correlation ID**: Unique identifier for grouping related transactions - **Metadata** (optional): Transaction-level JSON metadata - **Entry Metadata** (optional): Entry-level JSON metadata ## PUSH Workflow States PUSH workflows originate credit transactions with immediate settlement upon submission. **Workflow States:** 1. **CREATE**: Initial state creates encumbrance on customer account and charges fees. Funds reserved but not yet transmitted. 2. **CANCEL**: Reverses CREATE encumbrance before file generation. Optionally followed by REIMBURSE_FEE to refund processing fees. 3. **SUBMIT**: Final state settles funds, reversing encumbrance and posting to settled layer. Transaction included in next file generation. 4. **RETURN**: Processes return from RDFI, reversing SUBMIT settlement and returning funds to customer account. 5. **REIMBURSE_FEE**: Refunds processing fee, reversing original fee charge. **Key Characteristic:** PUSH workflows settle immediately on SUBMIT since credit transactions are final upon transmission. ## PULL Workflow States PULL workflows originate debit transactions with delayed settlement pending confirmation. **Workflow States:** 1. **CREATE**: Initial state creates encumbrance on settlement account (inverse of PUSH). Funds reserved awaiting collection. 2. **CANCEL**: Reverses CREATE encumbrance before file generation. 3. **SUBMIT**: Moves funds from encumbrance to pending layer. Transaction included in file but not yet final. 4. **SETTLE**: Final settlement moves funds from pending to settled layer after collection confirmation. 5. **RETURN**: Processes return from RDFI, reversing pending or settled amounts depending on when return received. 6. **REIMBURSE_FEE**: Refunds processing fee on cancellation or return. **Key Characteristic:** PULL workflows separate SUBMIT (transmission) from SETTLE (confirmation) since debit transactions require validation before finalization. ## Transaction Lifecycle and Ledger Accounting The ODFI processor uses multi-stage workflows with double-entry accounting at each stage, tracking funds through multiple balance layers. ### Balance Layers ODFI workflows utilize three balance layers to track transaction lifecycle and fund availability: #### Encumbrance Layer Temporary holds during CREATE state before transmission. Encumbered amounts don't affect available balance calculations but reserve funds for pending transactions. **PUSH (Credits):** ``` DR Customer Account (Encumbrance) CR Settlement Account (Encumbrance) ``` **PULL (Debits):** ``` DR Settlement Account (Encumbrance) CR Customer Account (Encumbrance) ``` The encumbrance layer allows you to: - Reserve funds before file generation - Track expected outflows (PUSH) or inflows (PULL) - Maintain visibility of in-flight transactions - Cancel transactions before file transmission #### Pending Layer PULL workflows only. Tracks transmitted debits awaiting settlement confirmation between SUBMIT and SETTLE states. ``` DR Settlement Account (Pending) CR Customer Account (Pending) ``` The pending layer represents: - Debits transmitted but not yet collected - Funds awaiting final confirmation from RDFI - Transactions that can still be returned #### Settled Layer Final layer for completed transactions. All fees post directly to settled layer. **PUSH SUBMIT:** ``` DR Customer Account (Settled) CR Settlement Account (Settled) ``` **PULL SETTLE:** ``` DR Settlement Account (Settled) CR Customer Account (Settled) ``` **Fees (all workflows):** ``` DR Customer Account (Settled) CR Fee Account (Settled) ``` The settled layer represents final, available balances that affect customer account availability. ### Balance Layer Illustration ``` PUSH (Credit) Flow: ┌─────────────────────────────────────────────────┐ │ CREATE: ENCUMBRANCE Layer │ │ DR Customer Account │ │ CR Settlement Account │ │ • Funds reserved, not yet transmitted │ └─────────────────────────────────────────────────┘ ↓ SUBMIT ┌─────────────────────────────────────────────────┐ │ SUBMIT: SETTLED Layer │ │ DR Customer Account │ │ CR Settlement Account │ │ • Final settlement, funds transmitted │ │ • Included in next file generation │ └─────────────────────────────────────────────────┘ PULL (Debit) Flow: ┌─────────────────────────────────────────────────┐ │ CREATE: ENCUMBRANCE Layer │ │ DR Settlement Account │ │ CR Customer Account │ │ • Collection reserved, not yet transmitted │ └─────────────────────────────────────────────────┘ ↓ SUBMIT ┌─────────────────────────────────────────────────┐ │ SUBMIT: PENDING Layer │ │ DR Settlement Account │ │ CR Customer Account │ │ • Transmitted, awaiting collection │ │ • Included in next file generation │ └─────────────────────────────────────────────────┘ ↓ SETTLE ┌─────────────────────────────────────────────────┐ │ SETTLE: SETTLED Layer │ │ DR Settlement Account │ │ CR Customer Account │ │ • Final settlement, funds collected │ │ • Customer account credited │ └─────────────────────────────────────────────────┘ ``` ### Querying Balances Check account balances across all layers using the standard balance query: ```graphql query GetAccountBalance { balance( accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" currency: "USD" ) { settled { crBalance { units } drBalance { units } } pending { crBalance { units } drBalance { units } } encumbrance { crBalance { units } drBalance { units } } version } } ``` **Available Balance Calculation:** ``` Available = Settled - Pending - Encumbrance (for debits) ``` ### Workflow State Transitions **PUSH Workflow States:** 1. `CREATE` - Reserve funds on customer account (encumbrance) 2. `SUBMIT` - Move to settled, queue for file generation 3. `RETURN` - Reverse settlement if RDFI returns transaction 4. `CANCEL` - Reverse encumbrance before file generation 5. `REIMBURSE_FEE` - Refund processing fee on cancellation/return **PULL Workflow States:** 1. `CREATE` - Reserve collection on settlement account (encumbrance) 2. `SUBMIT` - Move to pending, queue for file generation 3. `SETTLE` - Move to settled after successful collection 4. `RETURN` - Reverse pending/settled if RDFI returns transaction 5. `CANCEL` - Reverse encumbrance before file generation 6. `REIMBURSE_FEE` - Refund processing fee on cancellation/return ## File Generation File generation collects all submitted transactions and creates NACHA-formatted files for transmission to your financial institution or the Federal Reserve. ### Generating Files Use [`Mutation.ach.generateFile()`](/docs/reference/graphql/mutations#ach.generate-file) to create ACH files: ```graphql mutation GenerateODFIFile { ach { generateFile( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" fileKey: "outgoing-ach-20251114.ach" fileType: ODFI generateEmpty: false } ) { fileKey generated } } } ``` **File Types:** - `ODFI`: Combined file with both PUSH and PULL transactions - `ODFI_PUSH_ONLY`: Credit transactions only - `ODFI_PULL_ONLY`: Debit transactions only **Generation Control:** - `generateEmpty: true`: Always creates file (empty NACHA if no transactions) - `generateEmpty: false`: Only creates file when transactions exist ### What Gets Included File generation automatically collects: - All PUSH transactions in SUBMIT state - All PULL transactions in SUBMIT state (pending settlement) - Transactions grouped by effective date and SEC code - Proper batch headers and control totals - File-level hash totals and entry counts ### NACHA File Structure Generated files contain: 1. **File Header Record** - Configuration from `odfiHeaderConfiguration`: - Immediate destination (your ODFI routing number) - Immediate origin (your company ID) - File creation date/time - File ID modifier 2. **Batch Header Records** - One per batch: - Service class code (credits, debits, or mixed) - Company name and entry description - Effective entry date - ODFI identification 3. **Entry Detail Records** - One per transaction: - Transaction code (credit/debit, checking/savings) - RDFI routing number - Account number - Amount - Individual name - Trace number 4. **Batch Control Records** - Validates batch totals 5. **File Control Record** - Validates file totals ### Download Generated Files After generation, download files using [`Mutation.files.createDownload()`](/docs/reference/graphql/mutations#files.create-download): ```graphql mutation DownloadODFIFile { files { createDownload( key: "outgoing-ach-20251114.ach" ) { downloadURL } } } ``` Then download via HTTP GET: ```bash curl '' -o outgoing-ach-20251114.ach ``` Transmit the downloaded file to your ODFI via SFTP, FTPS, or your institution's preferred method. ### File Generation Timing **Best Practices:** - Generate files after your daily processing cutoff - Allow sufficient time for file transmission before ODFI deadlines - Consider timezone settings in your ACH configuration - Generate separate files for different effective dates **Standard ACH Deadlines:** - **Standard ACH**: Submit by 6:00 PM ET for next-day settlement - **Same-Day ACH**: Multiple submission windows (10:30 AM, 2:45 PM ET) - **Weekend Processing**: Transactions submitted Friday settle Monday ## Return Processing Return processing handles rejected transactions from RDFIs. When your ODFI provides a return file, Twisp automatically matches returns to original transactions and reverses ledger entries. ### Return Flow **1. Upload Return File** When you receive a return file from your ODFI, upload it using [`Mutation.files.createUpload()`](/docs/reference/graphql/mutations#files.create-upload): ```graphql mutation CreateReturnUpload { files { createUpload( input: { key: "return-20251114.ach" uploadType: ACH contentType: "text/plain" } ) { uploadURL } } } ``` Upload the file content: ```bash curl -T return-20251114.ach -XPUT '' ``` **2. Process Return File** Process the return file with `ODFI_RETURN` file type using [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file): ```graphql mutation ProcessReturnFile { ach { processFile( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" fileKey: "return-20251114.ach" fileType: ODFI_RETURN } ) { fileId } } } ``` **3. Automatic Return Matching** Twisp automatically: - Extracts trace numbers from return entries - Matches returns to original workflow executions - Executes RETURN workflow state for each transaction - Reverses appropriate balance layer entries - Updates workflow execution history - Creates complete audit trail via `AchWorkflowTrace` **4. Monitor Return Processing** Query file processing status using [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file): ```graphql query GetReturnFileStatus { ach { file( fileKey: "return-20251114.ach" configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" ) { fileId processingStatus processingDetail processingStatistics { numEntriesUnprocessed totalCreditAmount totalDebitAmount } } } } ``` ### Return Ledger Accounting Returns automatically reverse the appropriate balance layer entries. **PUSH Return (Credit Returned):** If a PUSH transaction in settled layer is returned: ``` Reverse original SUBMIT: DR Settlement Account (Settled) $1,500.00 CR Customer Account (Settled) $1,500.00 ``` Funds are returned to the customer account. **PULL Return (Debit Returned):** If a PULL transaction in pending layer is returned: ``` Reverse original SUBMIT: DR Customer Account (Pending) -$250.00 CR Settlement Account (Pending) -$250.00 ``` If a PULL transaction in settled layer is returned (after SETTLE executed): ``` Reverse original SETTLE: DR Customer Account (Settled) -$250.00 CR Settlement Account (Settled) -$250.00 ``` ### Common Return Codes Returns contain NACHA return codes indicating rejection reason: | Code | Reason | Common Cause | |------|--------|--------------| | `R01` | Insufficient Funds | Receiver account has insufficient balance | | `R02` | Account Closed | Receiver account has been closed | | `R03` | No Account / Unable to Locate | Account number not found at RDFI | | `R04` | Invalid Account Number | Account number fails validation | | `R05` | Unauthorized Debit | Consumer did not authorize debit | | `R07` | Authorization Revoked | Consumer revoked authorization | | `R08` | Payment Stopped | Receiver placed stop payment | | `R10` | Customer Advises Not Authorized | Receiver claims transaction unauthorized | | `R29` | Corporate Customer Not Authorized | Corporate receiver did not authorize | ### Return Timing **Standard Return Windows:** - **Most returns**: Within 2 business days of settlement - **Unauthorized returns** (R05, R07, R10): Up to 60 days after settlement - **Admin returns** (R02, R03, R04): Within 2 business days **Late Returns:** Some returns arrive after the standard 2-day window: - Still processed automatically by Twisp - May require manual reconciliation with your ODFI - Check `processingDetail` for any matching issues ### Handling Return Fees When a transaction is returned, you may want to reimburse processing fees: ```graphql mutation ReimburseFee { workflow { execute( input: { executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" code: "ACH_PUSH" task: "REIMBURSE_FEE" params: {} } ) { executionId output { state } } } } ``` **Ledger Entries:** ``` DR Fee Account (Settled) $0.25 CR Customer Account (Settled) $0.25 ``` ### Return Reconciliation Query workflow execution to see complete return history: ```graphql query GetReturnExecution { workflow { execution( executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" ) { workflowId task params activities { action entity { ... on Transaction { transactionId description entries(first:100) { nodes { accountId layer amount } } } ... on AchWorkflowTrace { traceNumber fileId } } } } } } ``` The `AchWorkflowTrace` entity links the return to the original transaction via trace number, enabling complete auditability. ## Transaction Tracking **Workflow Execution:** Each workflow execution receives unique `executionId` for tracking. Execution record contains: - Workflow and task identifiers - Input parameters - Output state - All created ledger transactions - ACH workflow traces with file and entry details - Complete history of state transitions **ACH Workflow Trace:** Links workflow execution to ACH file entries via: - Trace number (15-digit NACHA identifier) - File ID and record ID - Configuration ID and version - Workflow and execution identifiers Enables return matching, reconciliation, and complete transaction lineage tracking. ## Example : End-to-End ODFI Setup and Payment Complete flow from configuration through file transmission: ```graphql # 1. Create required accounts mutation CreateAccounts { # Settlement account - where funds transit during ACH processing settlement: createAccount( input: { accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" code: "settlement.ach" name: "ACH Settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } # Suspense account - for transactions to unknown accounts suspense: createAccount( input: { accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" code: "suspense.ach" name: "ACH Suspense" config: { enableConcurrentPosting: true } } ) { accountId } # Exception account - for failed transactions exception: createAccount( input: { accountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" code: "exception.ach" name: "ACH Exception" config: { enableConcurrentPosting: true } } ) { accountId } # Fee account - for ACH processing fees fee: createAccount( input: { accountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" code: "fee.ach" name: "ACH Fee Income" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId } # Customer account - for testing customer: createAccount( input: { accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" code: "customer.001" name: "Customer Account" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } } # 2. Create a journal for ACH transactions mutation CreateJournal { createJournal( input: { journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" name: "ACH Processing Journal" status: ACTIVE } ) { journalId } } # 3. Create a webhook endpoint (Note: webhook endpoint not used in ODFI-only use cases) mutation CreateACHWebhookProcessor { events { createEndpoint( input: { endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" status: ENABLED endpointType: ACH_PROCESSOR url: "https://webhook.site/ach-testing" subscription: [] description: "ACH webhook processor" } ) { endpointId } } } # 4. Create ACH configuration mutation CreateACHConfig { ach { createConfiguration( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" exceptionAccountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" odfiHeaderConfiguration: { immediateDestination: "021000021" immediateDestinationName: "Federal Reserve Bank" immediateOrigin: "1234567890" immediateOriginName: "Your Company Name" } timeZone: "America/New_York" } ) { configId version } } } mutation CreatePullTransaction { workflow { execute( input: { executionId: "c97685f6-c1b2-11f0-a959-069b540ea27c" code: "ACH_PULL" task: "CREATE" params: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" routingNumber: "026009593", accountNumber: "12345678901234567", accountType: "checking", individualName: "Clark Kent", entryDescription: "XFER", entryClassCode:"PPD" effective:"2025-11-14" amount: "250.00" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" feeAmount: "0.25" correlationId: "billing-batch-001" metadata: { customerId: "cust-456" invoiceNumber: "INV-2025-001" } entryMetadata: { companyName: "ACME Corp" companyEntryDescription: "INVOICE" individualName: "Jane Smith" standardEntryClassCode: "WEB" dfiAccountNumber: "987654321" identificationNumber: "inv-001" rdfiIdentification: "021000021" } } } ) { executionId output { state } } } } mutation GenerateODFIFile { ach { generateFile( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" fileKey: "outgoing-ach-20251114.ach" fileType: ODFI generateEmpty: false } ) { fileKey generated } } } mutation DownloadODFIFile { files { createDownload( key: "outgoing-ach-20251114.ach" ) { downloadURL } } } ``` ## Best Practices ### Security **API Key Management:** - Rotate API keys regularly - Use separate keys for production and development - Store keys in secure secret management systems (AWS Secrets Manager, HashiCorp Vault) - Never commit API keys to source control **Account Isolation:** - Use separate ACH configurations for different business units - Implement proper account-level access controls - Audit all ACH operations via workflow execution logs **Webhook Security (if using RDFI):** - Validate webhook signatures to ensure requests are from Twisp - Use HTTPS endpoints for all webhooks - Implement rate limiting and DDoS protection ### Performance **Batch Processing:** - Group transactions by effective date for efficient file generation - Use correlation IDs to track related transactions - Process CREATE and SUBMIT in parallel where possible **Account Configuration:** - Enable `enableConcurrentPosting: true` on all ACH accounts - This supports high-volume parallel transaction posting - Settlement account especially critical for concurrent access **File Generation Timing:** - Generate files during off-peak hours when possible - Separate file generation from transaction creation - Monitor file generation performance via [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file) ### Monitoring **Alert on Key Metrics:** - High return rates (> 2% indicates data quality issues) - Failed workflow executions - File generation failures - Settlement account balance anomalies **Daily Reconciliation:** - Compare file totals with workflow execution totals - Reconcile settlement account with ODFI reports - Track return rates by SEC code and effective date **Execution Tracking:** - Use correlation IDs to group related transactions - Query workflow executions for complete audit trails - Monitor workflow state transitions for stuck transactions ### Compliance **Record Retention:** - Maintain workflow execution history for 7 years minimum - Store generated ACH files and return files - Keep complete audit trail of all balance changes - Archive transaction metadata and authorization records **NACHA Rules:** - Adhere to return timeframes (2 business days for most codes) - Process returns within 24 hours of receipt - Maintain proper SEC codes for transaction types - Follow authorization requirements for debit transactions **Authorization Management:** - Store proof of authorization for debit transactions - Support authorization revocation (R07 returns) - Implement stop payment capabilities (R08 returns) - Handle unauthorized transaction disputes (R10 returns) **Reg E Compliance:** - Honor consumer dispute rights (60-day investigation period) - Provide proper disclosures for recurring debits - Implement error resolution procedures - Maintain consumer authorization records ### Error Handling **Transaction Failures:** - Monitor workflow execution failures - Implement retry logic for transient errors - Use CANCEL state to reverse failed transactions - Alert operations team for manual intervention **Return Handling:** - Process return files within 24 hours of receipt - Automatically reverse transactions via RETURN state - Consider reimbursing fees for returned transactions - Track return rates to identify systemic issues **File Generation Issues:** - Validate all transactions before SUBMIT - Test file generation with `generateEmpty: true` initially - Monitor file processing status via [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file) - Keep backup of generated files before transmission ## ODFI Operations Use GraphQL workflow API for all ODFI operations: - [`Mutation.workflow.execute()`](/docs/reference/graphql/mutations#workflow.execute): Execute workflow state transitions - [`Query.workflow.execution()`](/docs/reference/graphql/queries#workflow.execution): Get workflow execution details - [`Query.workflow.executions()`](/docs/reference/graphql/queries#workflow.executions): Query workflow executions File operations: - [`Mutation.ach.generateFile()`](/docs/reference/graphql/mutations#ach.generate-file): Generate NACHA file - [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file): Process return files - [`Query.ach.file()`](/docs/reference/graphql/queries#ach.file): Query file status ## Further Reading To learn ODFI operations from scratch, see the tutorial on [Your First ACH Payment](/docs/tutorials/ach/first-ach-payment). For production payment workflows, see the how-to guide on [Processing ACH Payments](/docs/guides/processing-ach-payments). For return management, see the how-to guide on [Handling ACH Returns](/docs/guides/handling-ach-returns). For complete GraphQL type definitions, see: - [WorkflowExecution](/docs/reference/graphql/types/object#workflow-execution) - [AchWorkflowTrace](/docs/reference/graphql/types/object#ach-workflow-trace) - [WorkflowExecuteInput](/docs/reference/graphql/types/input#workflow-execute-input) --- # RDFI Receive and process incoming ACH transactions as an RDFI Source: https://www.twisp.com/docs/reference/ach/rdfi ## Overview An RDFI (Receiving Depository Financial Institution) is the financial institution that receives ACH transactions on behalf of a receiver. When operating as an RDFI, the Twisp ACH processor handles incoming ACH files, validates transactions, updates account balances, and generates return files when necessary. The ACH RDFI processor enables you to: - Receive and process ACH credit and debit transactions - Apply transactions to customer accounts with proper ledger accounting - Handle exceptions through suspense and exception accounts - Generate return files for transactions that cannot be processed - Maintain complete audit trails through workflow execution tracking ## Getting Started ### Prerequisites Before processing RDFI files, you need: 1. **ACH Configuration** - Created via `Mutation.ach.createConfiguration()` 2. **Required Accounts** - Settlement, suspense, exception, and fee accounts 3. **Webhook Endpoint** - For receiving transaction decisioning requests 4. **Journal** - For posting transactions ### Quick Start Example ```graphql # 1. Create webhook endpoint for ACH decisioning mutation CreateEndpoint { events { createEndpoint( input: { endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" status: ENABLED endpointType: ACH_PROCESSOR url: "https://your-domain.com/webhooks/ach" subscription: [] description: "ACH RDFI webhook processor" } ) { endpointId } } } # 2. Create required accounts mutation CreateAccounts { # Settlement account - where funds transit settlement: createAccount( input: { accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" code: "settlement.ach" name: "ACH Settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } # Suspense account - for transactions to unknown accounts suspense: createAccount( input: { accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" code: "suspense.ach" name: "ACH Suspense" config: { enableConcurrentPosting: true } } ) { accountId } # Exception account - for failed transactions exception: createAccount( input: { accountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" code: "exception.ach" name: "ACH Exception" config: { enableConcurrentPosting: true } } ) { accountId } # Fee account - for ACH processing fees fee: createAccount( input: { accountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" code: "fee.ach" name: "ACH Fee Income" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId } } # 3. Create a journal for ACH transactions mutation CreateJournal { createJournal( input: { journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" name: "ACH Processing Journal" status: ACTIVE } ) { journalId } } # 4. Create ACH configuration mutation CreateACHConfig { ach { createConfiguration( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" exceptionAccountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" odfiHeaderConfiguration: { immediateDestination: "021000021" immediateDestinationName: "Your Bank Name" immediateOrigin: "1234567890" immediateOriginName: "Your Company Name" } timeZone: "America/New_York" } ) { configId version } } } ``` ## RDFI Workflow ### 1. Upload ACH File When you receive an ACH file from the Fed or your upstream processor, upload it to Twisp: ```graphql mutation CreateUpload { files { createUpload( input: { key: "incoming-ach-20251114.ach" uploadType: ACH contentType: "text/plain" } ) { uploadURL } } } ``` Upload the file using the returned URL: ```bash curl -T incoming-ach-20251114.ach -XPUT '' ``` ### 2. Process ACH File Start processing the uploaded file: ```graphql mutation ProcessFile { ach { processFile( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" fileKey: "incoming-ach-20251114.ach" fileType: RDFI } ) { fileId } } } ``` ### 3. Monitor File Processing Check the status of file processing: ```graphql query GetFileStatus { ach { file( fileKey: "incoming-ach-20251114.ach" configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" ) { fileId processingStatus processingDetail processingStatistics { numEntriesUnprocessed totalCreditAmount totalDebitAmount } } } } ``` **Processing Status Values:** - `NEW` - File created, not yet processing - `UPLOADED` - File uploaded and queued - `VALIDATING` - File format validation in progress - `PARTITIONING` - Preparing for parallel processing - `PROCESSING` - Sending webhooks for transaction decisions - `PROCESSED` - All webhooks sent, awaiting settlements - `COMPLETED` - All transactions settled or returned - `ERROR` - Unrecoverable error occurred - `INVALID` - File failed validation ### 4. Handle Transaction Webhooks For each ACH entry in the file, Twisp sends a webhook to your endpoint. You must respond with instructions on how to handle the transaction. **Webhook Payload Format:** ```json { "workflowName": "ACH.RDFI.CR", "workflowTask": "CREATE", "executionId": "60f7ac42-ff72-48c7-af58-ee1f9a2db1e0", "fileHeader": { "id": "file-header-id", "immediateDestination": "021000021", "immediateOrigin": "1234567890", "fileCreationDate": "251114", "fileCreationTime": "1030", "fileIDModifier": "A", "immediateDestinationName": "Your Bank Name", "immediateOriginName": "Originating Company", "referenceCode": "" }, "batchHeader": { "id": "batch-id", "serviceClassCode": "220", "companyName": "PAYROLL CO", "companyDiscretionaryData": "", "companyIdentification": "1234567890", "standardEntryClassCode": "PPD", "companyEntryDescription": "PAYROLL", "companyDescriptiveDate": "", "effectiveEntryDate": "251115", "settlementDate": " ", "originatorStatusCode": "1", "odfiIdentification": "12345678", "batchNumber": "0000001" }, "entryDetail": { "id": "entry-id", "transactionCode": "22", "rdfiIdentification": "02100002", "checkDigit": "1", "dfiAccountNumber": "123456789", "amount": "150000", "identificationNumber": "employee-123", "individualName": "John Doe", "discretionaryData": "", "addendaRecordIndicator": "0", "traceNumber": "123456780000001", "category": "Forward" } } ``` **Response Format:** You must respond with one of three actions: `SETTLE`, `RETURN`, or `RETRY`. #### Option 1: Settle (Accept Transaction) ```json { "action": "SETTLE", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "when": "2025-11-15T00:00:00.000Z", "metadata": { "customerId": "cust-123", "transactionType": "payroll" }, "entryMetadata": { "customerId": "cust-123" } } ``` - `when` is optional. If omitted, uses the effective date from the batch header - If `when` is in the past, the transaction settles immediately - `metadata` is optional and attached to the ledger transaction - `entryMetadata` is optional and attached to the ledger entries #### Option 2: Return (Reject Transaction) ```json { "action": "RETURN", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "addenda99": { "returnCode": "R01", "addendaInformation": "Insufficient Funds" }, "metadata": { "reason": "account_balance_insufficient" } } ``` **Common Return Codes:** - `R01` - Insufficient Funds - `R02` - Account Closed - `R03` - No Account / Unable to Locate Account - `R04` - Invalid Account Number - `R05` - Unauthorized Debit to Consumer Account - `R07` - Authorization Revoked by Customer - `R08` - Payment Stopped - `R10` - Customer Advises Not Authorized [See complete return code reference](#return-codes) #### Option 3: Retry (Temporary Error) ```json { "action": "RETRY" } ``` Use `RETRY` when: - Your system is temporarily unavailable - You need more time to make a decision - There's a transient error in your processing Twisp will exponentially back off and retry the webhook. ### 5. Generate Return File After processing is complete, generate a return file for any transactions you rejected: ```graphql mutation GenerateReturnFile { ach { generateFile( input: { configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" fileKey: "return-20251114.ach" fileType: RDFI_RETURN generateEmpty: false } ) { fileKey generated } } } ``` `generateEmpty: false` means the file is only created if there are returns to include. ### 6. Download Return File Download the generated return file: ```graphql mutation DownloadReturn { files { createDownload( key: "return-20251114.ach" ) { downloadURL } } } ``` Then download using the URL: ```bash curl '' -o return-20251114.ach ``` Transmit this file to the originating ODFI via your normal file transmission process (SFTP, etc.). ## Transaction Lifecycle and Ledger Accounting The RDFI processor uses a three-stage workflow with double-entry accounting at each stage. ### Stage 1: CREATE (Initial Encumbrance) When a transaction webhook is received, an **encumbrance** is created on the target account: **For Credits (Incoming Deposits):** ``` DR Settlement Account (Encumbrance Layer) CR Customer Account (Encumbrance Layer) ``` **For Debits (Outgoing Withdrawals):** ``` DR Customer Account (Encumbrance Layer) CR Settlement Account (Encumbrance Layer) ``` The encumbrance layer reserves funds but doesn't affect available balance. This allows you to: - Track expected funds before they settle - Maintain visibility of in-flight transactions - Reconcile with external ACH reports ### Stage 2: SETTLE (Final Settlement) When you respond with `"action": "SETTLE"`, two things happen: 1. **Reverse the encumbrance:** ``` Opposite of CREATE entries with negative amounts ``` 2. **Post to settled layer:** ``` DR/CR Customer Account (Settled Layer) DR/CR Settlement Account (Settled Layer) ``` The settled layer represents final, available balances that customers can access. ### Stage 3: RETURN (Rejection) When you respond with `"action": "RETURN"`, the transaction is reversed: 1. **Reverse the encumbrance** (same as SETTLE step 1) 2. **Post return to settled layer** (opposite direction of a normal settlement) 3. **Queue for return file generation** Returns are included in the next return file you generate via `Mutation.ach.generateFile()`. ### Balance Layer Illustration ``` ┌─────────────────────────────────────────────────┐ │ ENCUMBRANCE Layer │ │ • In-flight ACH transactions │ │ • Not available to customer │ │ • Tracks expected debits/credits │ └─────────────────────────────────────────────────┘ ↓ SETTLE ┌─────────────────────────────────────────────────┐ │ SETTLED Layer │ │ • Final, available balance │ │ • Customer can withdraw/spend │ │ • Appears in balance queries │ └─────────────────────────────────────────────────┘ ``` ### Querying Balances Check account balances across all layers: ```graphql query GetAccountBalance { balance( accountId: "d2f7183f-8e9c-45e7-9a98-ef1897ddb930" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" currency: "USD" ) { settled { crBalance { units currency } drBalance { units currency } } pending { crBalance { units currency } drBalance { units currency } } encumbrance { crBalance { units currency } drBalance { units currency } } version } } ``` **Available Balance Calculation:** ``` Available = Settled - Pending - Encumbrance (for debits) ``` ## Return Generation Create returns for transactions that cannot be processed: ### Return Decision Logic Automatic and manual return triggers: - **Insufficient Funds**: Account balance insufficient for debit - **Account Closed**: Target account no longer active - **Invalid Account**: Account number not found - **Unauthorized**: Transaction not authorized by account holder - **Stop Payment**: Account holder placed stop payment order ### Return Codes Select appropriate return code: - **R01**: Insufficient Funds - **R02**: Account Closed - **R03**: No Account / Unable to Locate Account - **R04**: Invalid Account Number - **R05**: Unauthorized Debit to Consumer Account (improper authorization) - **R07**: Authorization Revoked by Customer - **R08**: Payment Stopped - **R10**: Customer Advises Not Authorized - **R29**: Corporate Customer Advises Not Authorized ### Return Timing Return deadlines by code: - **2 Business Days**: Most return codes (R01-R04, R07-R08, etc.) - **60 Calendar Days**: Unauthorized returns (R05, R07, R10, R29) - **Next Business Day**: Same-day ACH returns ### Return File Generation Create NACHA return files: - **Return Entry**: Create return detail record in Twisp - **Return Batch**: Group returns in batches - **Return File Generation**: Generate complete NACHA return file via Files API - **File Download**: Retrieve generated return file from Twisp - **File Transmission**: You transmit return file to originating ODFI via SFTP/FTPS ## Monitoring and Observability ### Query Files by Status Find all files in a specific processing state: ```graphql query GetProcessingFiles { ach { files( first: 100 index: { name: PROCESSING_STATUS } where: { configId: { eq: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" } processingStatus: { eq: "PROCESSING" } created: { gte: "2025-11-01T00:00:00Z" } } ) { nodes { fileId fileKey processingStatus processingDetail processingStatistics { numEntriesUnprocessed totalCreditAmount totalDebitAmount } created modified } pageInfo { hasNextPage endCursor } } } } ``` ### Track Workflow Execution Every ACH transaction creates a workflow execution that you can query: ```graphql query GetWorkflowExecution { workflow { execution( executionId: "60f7ac42-ff72-48c7-af58-ee1f9a2db1e0" ) { workflowId executionId task params output { state } activities { action entityType entityId entity { ... on Transaction { transactionId effective description } ... on AchWorkflowTrace { traceNumber fileId configId } } } created modified version } } } ``` The `activities` field shows all transactions and ACH traces created by this workflow, giving you complete auditability. ## Exception Handling ### Suspense Account When a transaction targets an account that doesn't exist, it's automatically posted to your configured `suspenseAccountId`. This allows you to: 1. Accept the transaction (avoiding a return) 2. Research the correct account 3. Create a manual journal entry to move funds to the correct account **Example scenario:** - ACH credit arrives for account number "123456789" - Account doesn't exist in your system - Transaction is posted: ``` DR Settlement Account CR Suspense Account ``` - You investigate and find the correct account is "123456790" - You create a journal entry to move the funds: ``` DR Suspense Account CR Correct Customer Account ``` ### Exception Account When a transaction fails due to velocity controls, account state issues, or other processing errors, it's posted to your `exceptionAccountId`. Common scenarios: - **Velocity Control Violation**: Transaction exceeds configured velocity limits - **Account Locked**: Target account is frozen or locked - **Processing Error**: Temporary system issue Funds in the exception account should typically be returned to the originator via a return file. ### Velocity Control Integration The RDFI processor respects velocity controls configured on customer accounts. If a debit would exceed the velocity limit: 1. The webhook is NOT sent to your endpoint 2. The transaction is automatically posted to the exception account 3. A return will be generated with code R01 (Insufficient Funds) This provides built-in protection against overdrafts and unauthorized transactions. ## Practical Examples ### Example 1: Basic RDFI Setup and Processing Complete flow from setup to settlement: ```graphql # Step 1: Setup (run once) mutation Setup { # Create webhook endpoint endpoint: events { createEndpoint( input: { endpointId: "webhook-001" status: ENABLED endpointType: ACH_PROCESSOR url: "https://api.yourcompany.com/ach/webhook" subscription: [] } ) { endpointId } } # Create journal journal: createJournal( input: { journalId: "journal-001" name: "ACH Journal" status: ACTIVE } ) { journalId } # Create accounts (abbreviated) settlement: createAccount( input: { accountId: "acct-settlement" code: "settlement" name: "ACH Settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } # Create ACH config config: ach { createConfiguration( input: { configId: "config-001" endpointId: "webhook-001" journalId: "journal-001" settlementAccountId: "acct-settlement" suspenseAccountId: "acct-suspense" exceptionAccountId: "acct-exception" feeAccountId: "acct-fee" odfiHeaderConfiguration: { immediateDestination: "021000021" immediateDestinationName: "Federal Reserve Bank" immediateOrigin: "1234567890" immediateOriginName: "Your Company" } timeZone: "America/New_York" } ) { configId } } } # Step 2: Upload file (when received from Fed) mutation UploadFile { files { createUpload( input: { key: "incoming-20251114-001.ach" uploadType: ACH contentType: "text/plain" } ) { uploadURL } } } # Use uploadURL to PUT file content # Step 3: Process file mutation ProcessFile { ach { processFile( input: { configId: "config-001" fileKey: "incoming-20251114-001.ach" fileType: RDFI } ) { fileId } } } # Step 4: Monitor processing query MonitorFile { ach { file( fileKey: "incoming-20251114-001.ach" configId: "config-001" ) { processingStatus processingStatistics { numEntriesUnprocessed totalCreditAmount totalDebitAmount } } } } # Step 5: Generate returns (after webhooks complete) mutation GenerateReturns { ach { generateFile( input: { configId: "config-001" fileKey: "return-20251114-001.ach" fileType: RDFI_RETURN generateEmpty: false } ) { fileKey generated } } } # Step 6: Download returns mutation DownloadReturns { files { createDownload( key: "return-20251114-001.ach" ) { downloadURL } } } ``` ### Example 2: Webhook Handler Implementation Sample webhook handler in Node.js: ```javascript app.post('/ach/webhook', async (req, res) => { const { workflowName, workflowTask, executionId, entryDetail } = req.body; try { // Extract transaction details const accountNumber = entryDetail.dfiAccountNumber; const amount = parseFloat(entryDetail.amount) / 100; // Amount is in cents const isDebit = entryDetail.transactionCode.startsWith('2'); // 22, 23, 24 const isCredit = entryDetail.transactionCode.startsWith('3'); // 32, 33, 34 // Look up customer account const account = await findAccountByNumber(accountNumber); if (!account) { // Account not found - will go to suspense return res.json({ action: 'SETTLE', accountId: SUSPENSE_ACCOUNT_ID, metadata: { reason: 'account_not_found', originalAccountNumber: accountNumber } }); } if (account.status === 'CLOSED') { // Account closed - return with R02 return res.json({ action: 'RETURN', accountId: account.id, addenda99: { returnCode: 'R02', addendaInformation: 'Account Closed' } }); } if (isDebit) { // Check balance for debits const balance = await getAccountBalance(account.id); if (balance < amount) { return res.json({ action: 'RETURN', accountId: account.id, addenda99: { returnCode: 'R01', addendaInformation: 'Insufficient Funds' } }); } } // All checks passed - settle the transaction return res.json({ action: 'SETTLE', accountId: account.id, when: new Date().toISOString(), // Settle immediately metadata: { customerId: account.customerId, originalTraceNumber: entryDetail.traceNumber } }); } catch (error) { console.error('Webhook processing error:', error); // Retry on errors return res.json({ action: 'RETRY' }); } }); ``` ## API Reference ### GraphQL Operations **Configuration:** - `Query.ach.configuration(id: UUID!)` - Get ACH configuration - `Query.ach.configurations(first: Int!)` - List all configurations - `Mutation.ach.createConfiguration(input: AchCreateConfigurationInput!)` - Create configuration - `Mutation.ach.updateConfiguration(configId: UUID!, input: AchUpdateConfigurationInput!)` - Update configuration **File Operations:** - `Query.ach.file(id: UUID, fileKey: String, configId: UUID)` - Get file status - `Query.ach.files(index: AchFileInfoIndexInput!, where: AchFileInfoFilterInput!, first: Int!)` - Query files - `Mutation.ach.processFile(input: AchProcessFileInput!)` - Process uploaded file - `Mutation.ach.generateFile(input: AchGenerateFileInput!)` - Generate return/NOC file - `Mutation.files.createUpload(input: CreateUploadInput!)` - Get upload URL - `Mutation.files.createDownload(key: String!)` - Get download URL **Workflow Operations:** - `Query.workflow.execution(executionId: UUID!)` - Get workflow execution details ## Return Codes Reference When returning a transaction, use the appropriate return code in the `addenda99.returnCode` field. ### Standard Return Codes | Code | Reason | Description | Timing | |------|--------|-------------|--------| | `R01` | Insufficient Funds | Available balance is not sufficient to cover the dollar value of the debit entry | 2 business days | | `R02` | Account Closed | Previously active account has been closed by customer or RDFI | 2 business days | | `R03` | No Account/Unable to Locate Account | Account number structure is valid and passes editing process, but does not correspond to individual or is not an open account | 2 business days | | `R04` | Invalid Account Number | Account number structure not valid; entry may fail check digit validation or may contain an incorrect number of digits | 2 business days | | `R05` | Improper Debit to Consumer Account | A CCD, CTX, or CBR debit entry was transmitted to a Consumer Account of the Receiver and was not authorized by the Receiver | 60 days | | `R06` | Returned per ODFI's Request | ODFI has requested RDFI to return the ACH entry (optional to RDFI - ODFI indemnifies RDFI) | 2 business days | | `R07` | Authorization Revoked by Customer | Consumer, who previously authorized ACH payment, has revoked authorization from Originator | 60 days | | `R08` | Payment Stopped | Receiver of a recurring debit transaction has stopped payment to a specific ACH debit | 2 business days | | `R09` | Uncollected Funds | Sufficient book or ledger balance exists to satisfy dollar value of the transaction, but the dollar value of transaction is in process of collection | 2 business days | | `R10` | Customer Advises Originator is Not Known to Receiver and/or Originator is Not Authorized | The receiver does not know the Originator's identity and/or has not authorized the Originator to debit | 60 days | | `R11` | Customer Advises Entry Not in Accordance with the Terms of the Authorization | The Originator and Receiver have a relationship, and an authorization to debit exists, but there is an error or defect in the payment | 60 days | | `R12` | Branch Sold to Another DFI | Financial institution receives entry destined for an account at a branch that has been sold to another financial institution | 2 business days | | `R13` | RDFI not qualified to participate | Financial institution does not receive commercial ACH entries | 2 business days | | `R14` | Representative payee deceased or unable to continue in that capacity | The representative payee authorized to accept entries on behalf of a beneficiary is either deceased or unable to continue in that capacity | 2 business days | | `R15` | Beneficiary or bank account holder deceased | (1) the beneficiary entitled to payments is deceased or (2) the bank account holder other than a representative payee is deceased | 2 business days | | `R16` | Bank account frozen | Funds in bank account are unavailable due to action by RDFI or legal order | 2 business days | | `R17` | File record edit criteria | Fields rejected by RDFI processing (identified in return addenda) | 2 business days | | `R20` | Non-payment bank account | Entry destined for non-payment bank account defined by regulation | 2 business days | | `R23` | Credit entry refused by receiver | Receiver returned entry because minimum or exact amount not remitted, bank account is subject to litigation, or payment represents an overpayment | 2 business days | | `R29` | Corporate customer advises not authorized | RDFI has been notified by corporate receiver that debit entry of originator is not authorized | 2 business days | ### Best Practices **Return Timing:** - Most returns must be sent within **2 business days** of settlement date - Unauthorized returns (R05, R07, R10, R11) can be returned up to **60 days** after settlement - Same-day ACH returns must be sent by the same business day **Common Scenarios:** ```javascript // Insufficient funds { "action": "RETURN", "accountId": "account-id", "addenda99": { "returnCode": "R01", "addendaInformation": "Insufficient Funds" } } // Account closed { "action": "RETURN", "accountId": "account-id", "addenda99": { "returnCode": "R02", "addendaInformation": "Account Closed" } } // Account not found { "action": "RETURN", "accountId": "account-id", "addenda99": { "returnCode": "R03", "addendaInformation": "No Account" } } // Unauthorized transaction { "action": "RETURN", "accountId": "account-id", "addenda99": { "returnCode": "R10", "addendaInformation": "Not Authorized" } } ``` ## Best Practices ### Security - **Webhook Authentication**: Validate webhook signatures to ensure requests are from Twisp - **HTTPS Only**: Always use HTTPS endpoints for webhooks - **Idempotency**: Handle duplicate webhooks gracefully using `executionId` ### Performance - **Fast Webhook Response**: Respond to webhooks within 30 seconds - **Async Processing**: Queue webhook processing if complex logic is needed - **Retry Logic**: Implement exponential backoff for webhook retries ### Monitoring - **Alert on Status Changes**: Monitor file processing status for errors - **Track Return Rates**: High return rates may indicate data quality issues - **Balance Reconciliation**: Daily reconciliation of settlement account ### Compliance - **Return Timeframes**: Adhere to NACHA return deadlines (2 days for most codes) - **Authorization Records**: Maintain proof of authorization for debits - **Transaction History**: Keep complete audit trail for 7 years - **Reg E Compliance**: Honor consumer dispute rights (60-day investigation period) ## Further Reading For practical guidance on receiving ACH transactions: - [Handling ACH Returns and NOCs](/docs/guides/handling-ach-returns) - Return processing and NOC management - [Reconciling ACH Files](/docs/guides/reconciling-ach-files) - File validation and reconciliation procedures - [Processing ACH Payments](/docs/guides/processing-ach-payments) - Risk management and best practices For additional technical details: - [Configuration](/docs/reference/ach/configuration) - RDFI configuration parameters - [File Operations](/docs/reference/ach/file-operations) - File upload and parsing API specifications - [ODFI Reference](/docs/reference/ach/odfi) - Origination perspective - [ACH Processor](/docs/processors/ach) - Conceptual overview --- # Authentication Authenticating requests against the Twisp API. Source: https://www.twisp.com/docs/reference/api/authentication --- # Errors Error codes returned by the Twisp API when things don't go right. Source: https://www.twisp.com/docs/reference/api/errors This reference lists the types of errors that may be encountered. ## Error Responses The API uses a response format conforming to the [GraphQL spec](https://spec.graphql.org/October2021/#sec-Response-Format). For more information, see the docs on [Response Format](/docs/reference/api/response-format). > If the request raised any errors, the response map must contain an entry with key `errors`. The value of this entry is described in the “Errors” section. If the request completed without raising any errors, this entry must not be present. Because the Twisp API supports [Transactional Operations](/docs/reference/api/transactional-operations), if there are _any_ errors, then the entire operation is aborted and the `"data"` field will be `null`. ## Example Response This example shows the response of an `createAccount` operation with a malformed `accountId`. **Request** ```graphql mutation INVALID_UUID_LENGTH { createAccount( input: { accountId: "" code: "TEST" name: "TEST" normalBalanceType: DEBIT status: ACTIVE } ) { accountId } } ``` **Response** ```json { "errors": [ { "message": "input: createAccount.input.accountId invalid UUID length: 0", "path": ["createAccount", "input", "accountId"], "extensions": { "code": "UUID_PARSE_ERROR", "retriableError": false } } ], "data": null } ``` The response indicates that there was an error with the `createAccount` mutation request. The error is a `UUID_PARSE_ERROR`, which means that the `accountId` field in the request has an invalid UUID value. Within the `errors` array, each error object contains fields specifying more information about the error: - `message` summarizes the error, and may give a clue to its cause. In this example, it indicates that the `accountId` field has an invalid length, which is 0. - `path` specifies the location within the GraphQL operation that the error occurred. In this example, it indicates that the error occurred in the `accountId` field of the `input` argument provided to the `createAccount` mutation. - `extensions` provides additional information about the error. - `extensions.code` specifies the **error code** used to represent the error. In this example, the code is `UUID_PARSE_ERROR`. The `data` field in the response is `null` since the request was not successful due to the error. To resolve this particular error, a valid UUID value should be provided for the `accountId` field. An empty string is not a valid UUID value. ## Error Codes ### ACCESS_DENIED Indicates that the request was denied due to a lack of proper authentication or authorization. ### ALREADY_EXISTS Indicates that the requested resource already exists and cannot be created again. ### BAD_REQUEST Indicates that the request was malformed or invalid. For example, a missing `eq` in the partition key: **Request** ```graphql query EqRequiredForPartitionKey { accounts( index: { name: ACCOUNT_ID } where: { accountId: { like: "foo" } } first: 10 ) { nodes { accountId name } } } ``` **Response** ```json { "errors": [ { "message": "input: accounts eq required for account_id partition key", "path": ["accounts"], "extensions": { "code": "BAD_REQUEST", "retriableError": false } } ], "data": null } ``` Or a missing partition key: **Request** ```graphql query PartitionKeyRequired { accounts( index: { name: ACCOUNT_ID } where: { name: { eq: "TESTACCT" } } first: 10 ) { nodes { accountId name } } } ``` **Response** ```json { "errors": [ { "message": "input: accounts account_id partition key required", "path": ["accounts"], "extensions": { "code": "BAD_REQUEST", "retriableError": false } } ], "data": null } ``` ### CEL_EVALUATION_ERROR Indicates that there was an error evaluating a CEL expression. ### DATE_PARSE_ERROR Indicates that there was an error parsing a date string. **Request** ```graphql mutation DATE_PARSE_ERROR { postTransaction( input: { transactionId: "981b7bbd-2d70-4975-90d3-027ee9d8be77" tranCode: "TESTTC" params: { accountId: "7052f9fd-dd15-40d0-b0b6-df8c4c372e41" effectiveDate: "{2022-12-21}" } } ) { transactionId } } ``` **Response** ```json { "errors": [ { "message": "input: postTransaction parsing time \"{2022-12-21}\" as \"2006-01-02\": cannot parse \"{2022-12-21}\" as \"2006\"", "path": ["postTransaction"], "extensions": { "code": "DATE_PARSE_ERROR", "retriableError": false } } ], "data": null } ``` ### DEPENDENCY_ERROR Indicates that there was an error with a dependent resource. **Request** ```graphql mutation DEPENDENCY_ERRORS { missingRequiredParam: postTransaction( input: { transactionId: "f82391bb-90a7-4328-b5ad-8629a7602de8" tranCode: "TESTTC" params: { accountId: "7052f9fd-dd15-40d0-b0b6-df8c4c372e41" } } ) { transactionId } } ``` **Response** ```json { "errors": [ { "message": "input: missingRequiredParam param 'effectiveDate' not defined", "path": ["missingRequiredParam"], "extensions": { "code": "DEPENDENCY_ERROR", "retriableError": false } } ], "data": null } ``` ### ENUM_PARSE_ERROR Indicates that there was an error parsing an enumeration value. **Request** ```graphql query GetJournalsInvalidStatus { journals( index: { name: STATUS } where: { status: { eq: "FOO" } } first: 100 ) { nodes { name status created } } } ``` **Response** ```json { "errors": [ { "message": "input: journals journal status enum 'FOO' not found", "path": ["journals"], "extensions": { "code": "ENUM_PARSE_ERROR", "retriableError": false } } ], "data": null } ``` ### FOREIGN_KEY_VIOLATION Indicates that there was an error with a foreign key constraint. ### GRAPHQL_PARSE_FAILED Indicates that there was an error parsing the GraphQL query. **Request** ```graphql mutation 82* ``` **Response** ```json { "errors": [ { "message": "Expected {, found Int", "locations": [{ "line": 1, "column": 10 }], "extensions": { "code": "GRAPHQL_PARSE_FAILED", "retriableError": false } } ], "data": null } ``` ### GRAPHQL_VALIDATION_FAILED Indicates that there was an error validating the GraphQL query. **Request** ```graphql query first_should_be_int { accounts(first: "10") { nodes { accountId } } } ``` **Response** ```json { "errors": [ { "message": "Int cannot represent non-integer value: \"10\"", "locations": [{ "line": 2, "column": 20 }], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED", "retriableError": false } }, { "message": "Field \"accounts\" argument \"index\" of type \"AccountIndexInput!\" is required, but it was not provided.", "locations": [{ "line": 2, "column": 3 }], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED", "retriableError": false } } ], "data": null } ``` ### INTERNAL_SERVER_ERROR Indicates that there was an error on the server that prevented it from fulfilling the request. ### INTERRUPTED Indicates that the request was interrupted. ### JSON_PARSE_ERROR Indicates that there was an error parsing a JSON string. **Request** ```graphql mutation JSON_PARSE_ERROR { invalidParams: postTransaction( input: { transactionId: "e0995c6f-1a25-42f2-b2f2-568cf91f0487" tranCode: "TESTTC" params: "" } ) { transactionId } } ``` **Response** ```json { "errors": [ { "message": "input: invalidParams.input.params invalid JSON format", "path": ["invalidParams", "input", "params"], "extensions": { "code": "JSON_PARSE_ERROR", "retriableError": false } } ], "data": null } ``` ### NOT_FOUND Indicates that the requested resource was not found. ### NOT_SUPPORTED Indicates that the requested operation is not supported. ### TIMESTAMP_PARSE_ERROR Indicates that there was an error parsing a timestamp. ### TRAN_CODE_ERROR Indicates that there was an error with a transaction code. For example, with unbalanced entries: **Request** ```graphql mutation CreateTranCode { createTranCode( input: { tranCodeId: "74e90f0a-5ffa-4fce-9880-1bc976b59937" code: "TFR_TEST" description: "Transfer $1 from one account to another" params: [ { name: "effectiveDate", type: DATE } { name: "fromAccount" type: UUID description: "Account to send funds from." } { name: "toAccount" type: UUID description: "Recipient account. Required." } { name: "amount", type: DECIMAL, default: "1.00" } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effectiveDate" } entries: [ { accountId: "params.fromAccount" units: "params.amount" currency: "'USD'" description: "metadata.tags + ' from'" entryType: "'TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } ] metadata: { tags: "xfer" } } ) { tranCodeId code transaction { effective } entries { accountId units entryType direction layer } metadata } } ``` **Response** ```json { "errors": [ { "message": "input: createTranCode 'TFR_TEST' tran code entries unbalanced: 'DR 1.00 USD != 'CR 0 USD'", "path": ["createTranCode"], "extensions": { "code": "TRAN_CODE_ERROR", "retriableError": false } } ], "data": null } ``` Another example, with a syntax error: **Request** ```graphql mutation TRAN_CODE_ERROR { # Invalid CEL syntax in transaction.effective tc1: createTranCode( input: { tranCodeId: "B852FEA6-3445-49E7-BFD4-A162EEC08017" code: "ABC123" description: "an example tran code" params: [{ name: "amount", type: DECIMAL }] transaction: { effective: "{time.Now()}" journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" } entries: [ { entryType: "EXAMPLE" accountId: "uuid('7052f9fd-dd15-40d0-b0b6-df8c4c372e41')" layer: "SETTLED" units: "params.amount" currency: "'USD'" direction: DEBIT } ] } ) { code status } # Invalid CEL syntax in transaction.journalId tc2: createTranCode( input: { tranCodeId: "B852FEA6-3445-49E7-BFD4-A162EEC08018" code: "DEF456" description: "an example tran code" params: [{ name: "amount", type: DECIMAL }] transaction: { effective: "time.Now()" journalId: "822cb59f-ce51-4837-8391-2af3b7a5fc51" } entries: [ { entryType: "EXAMPLE" accountId: "uuid('7052f9fd-dd15-40d0-b0b6-df8c4c372e41')" layer: "SETTLED" units: "params.amount" currency: "'USD'" direction: DEBIT } ] } ) { code status } } ``` **Response** ```json { "errors": [ { "message": "input: tc1 [effective]: `{time.Now()}`, ERROR: :1:12: Syntax error: mismatched input '}' expecting ':'\n | {time.Now()}\n | ...........^", "path": ["tc1"], "extensions": { "code": "TRAN_CODE_ERROR", "retriableError": false } }, { "message": "input: tc2 [journal_id]: `822cb59f-ce51-4837-8391-2af3b7a5fc51`, ERROR: :1:4: Syntax error: mismatched input 'cb59f' expecting \n | 822cb59f-ce51-4837-8391-2af3b7a5fc51\n | ...^", "path": ["tc2"], "extensions": { "code": "TRAN_CODE_ERROR", "retriableError": false } } ], "data": null } ``` ### TRANSACTION_ERROR Indicates that there was an error with a transaction. ### UNIQUE_CONSTRAINT_VIOLATION Indicates that there was an error with a unique constraint. **Request** ```graphql mutation UNIQUE_CONSTRAINT_VIOLATION { # Account already exists createAccount( input: { accountId: "7052f9fd-dd15-40d0-b0b6-df8c4c372e41" code: "DUP_TESTACCT" name: "DUP_TESTACCT" normalBalanceType: DEBIT status: ACTIVE } ) { accountId } } ``` **Response** ```json { "errors": [ { "message": "input: createAccount unique constraint violation\nAccount.indexes.account_id unique constraint violation", "path": ["createAccount"], "extensions": { "action": "", "code": "UNIQUE_CONSTRAINT_VIOLATION", "path": "system.Account", "retriableError": false, "system": ["full_access_auth", "tx"] } } ], "data": null } ``` ### UNKNOWN_ERROR Indicates that an unknown error occurred. For example, an `unknown error` occuring when selecting accounts ```json { "errors": [ { "message": "input: accounts unknown error", "path": [ "accounts" ], "extensions": { "code": "UNKNOWN_ERROR", "retriableError": true } } ], "data": null } ``` ### UUID_PARSE_ERROR Indicates that there was an error parsing a UUID. **Request** ```graphql mutation INVALID_UUID_LENGTH { createAccount( input: { accountId: "" code: "TEST" name: "TEST" normalBalanceType: DEBIT status: ACTIVE } ) { accountId } } ``` **Response** ```json { "errors": [ { "message": "input: createAccount.input.accountId invalid UUID length: 0", "path": ["createAccount", "input", "accountId"], "extensions": { "code": "UUID_PARSE_ERROR", "retriableError": false } } ], "data": null } ``` --- # Extensions The Twisp GraphQL API supplies additional metadata about operations through the "extensions" subdocument. Source: https://www.twisp.com/docs/reference/api/extensions API responses may contain an `"extensions"` field in addition to the standard `"data"` field if certain request headers are specified. ## API Usage & Billing Billing stats about the current request may be returned within the `"extensions"` subdocument by including a `x-twisp-include-billing: true` header in the request. For example, here's is a response from invoking a bank transfer tran code with `postTransaction` and the `x-twisp-include-billing` HTTP header set to `true`: ```json { "data": { "postTransaction": { "transactionId": "5c328550-bba3-423b-a58a-b3f9786a80ad" }, }, "extensions": { "billing": { "duration": 229272730, "read": { "bytes": 5604, "count": 10, "units": 10 }, "write": { "bytes": 4850, "count": 16, "units": 16 } } } } ``` > **Note:** > > If using the gRPC apis the billing information will be emitted on the following trailer keys: > > > - billing-read-bytes > - billing-read-units > - billing-write-bytes > - billing-write-units > - billing-duration --- # API Requests via HTTPS Components of a GraphQL API request. Source: https://www.twisp.com/docs/reference/api/https-requests All HTTPS requests (whether they are GraphQL [Queries](/docs/reference/graphql/queries) or [Mutations](/docs/reference/graphql/mutations)) are made using the `POST` method, with the GraphQL operation provided in the request body. > **Note:** > > For a step-by-step guide, see the [Making Api Requests With Curl](/docs/tutorials/api/making-api-requests-with-curl) tutorial. When you are ready to start making requests to the Twisp API from your application code, the HTTPS request must include: - **URL**: the URL of the Twisp GraphQL API. - **Headers**: to authorize the request and specify content type. At minimum, these must include: - `authorization` with a bearer JWT - `x-twisp-account-id` for specifying the account ID to be used in the request - `content-type` set to `application/json` - ***Body**: the JSON-formatted GraphQL operation. Here is an example request for the first 10 accounts with a code starting with `BERT.`: ```shell curl 'https://api.us-east-1.cloud.twisp.com/financial/v1/graphql' \ -H 'authorization: Bearer ' \ -H 'content-type: application/json' \ -H 'x-twisp-account-id: ' \ --data-raw '{"query":"query { accounts(index:{name:CODE}, where:{code:{like:\"BERT.\"}},first:10) { nodes { accountId name code } } }"}' ``` The `` and `` are placeholders. If you created a [Tenant](/docs/reference/graphql/types/object#tenant) named "Sandbox" with account ID `Sandbox0d6af983` and had a JWT encoded as `0cca9e8f` (obviously it would be much longer in actual usage), then you would replace `` with `Sandbox0d6af983` and `` with `0cca9e8f`. > **Note:** > > As you are first starting out with Twisp, we recommend using the GraphiQL interface in the Twisp console. It is a rich learning environment where you can focus on writing your GraphQL operations with built-in documentation and autocomplete. --- # API Reference Key components and concepts for interacting with the API. Source: https://www.twisp.com/docs/reference/api - [Requests](/docs/reference/api/https-requests): Structure of the HTTP request. - [Response Format](/docs/reference/api/response-format): Structure of the JSON response. - [Pagination](/docs/reference/api/pagination): Cursor-based pagination for queries and fields. - [Errors](/docs/reference/api/errors): Error codes returned by the API. - [Transactional Operations](/docs/reference/api/transactional-operations): All-or-nothing semantics of API requests. - [Extensions](/docs/reference/api/extensions): Metadata about API requests. --- # Indexes Queries against the ledger are done via database indexes. This document explains how to use them. Source: https://www.twisp.com/docs/reference/api/indexes --- # Pagination When making queries which can potentially return high-cardinality lists, the Twisp API supports cursor-based pagination. Source: https://www.twisp.com/docs/reference/api/pagination The Twisp GraphQL API implements a cursor-based pagination model using the [Relay GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). This specification provides guidelines for making paginated requests using a concept called "Connections." Connections facilitate the process of fetching data in chunks (or pages) from a GraphQL API. > **Note:** > > For a step-by-step guide on using pagination in a GraphQL operation, see the [Querying Paginated Fields](/docs/tutorials/api/querying-paginated-fields) tutorial. ## Heuristics for Paginated Queries Within the GraphQL schema, pluralized query fields (e.g. `Query.entries`, `Query.tranCodes`) return Connection types. No matter what type of record we query, the same pattern can be applied. You can use these basic heuristics across the schema: - When querying a field with a `*Connection` type response, you must indicate the number of records to return with the `first` argument. - To determine if there are additional records beyond the current page, query the `pageInfo` object. - To get the next page of `n` records, use the cursor provided `pageInfo.endCursor` as the `after` argument. - When there are no more records, `pageInfo.hasNextPage` will be false. This is an abstracted example of an imaginary `widgets` query to return `Widget` records: ```graphql query { widgets( after: "" # Cursor indicating the start point. first: 5 # Number of nodes (widgets) to return. ) { __typename # => WidgetConnection edges { __typename # => WidgetConnectionEdge cursor # Cursor position of the current edge. node { # The Widget record at this edge position. # ... # Field queries on the Widget record. } } nodes { # Alternate access to all `node` objects within `edges` __typename # => Widget # ... # Field queries on the Widget record. } pageInfo { hasPreviousPage # True if there are nodes in the connection before the current page / start cursor. hasNextPage # True if there are nodes in the connection after the current page / end cursor. startCursor # Query cursor for the first node in the current page. endCursor # Query cursor for the last node in the current page. } } } ``` The rest of this reference page will go into the specifics of cursor-based pagination using the `accounts` query as an example, but the same principles can be applied to every query resolving to a `*Connection` type. ## Querying Lists with the First Argument When making queries to fields which resolve to paginated lists of records, the required `first` argument is used to specify the _maximum_ number of records to return. For example, when using the `journals` query, we can request only the first 5 journals using the following query. We query the `nodes` field to get fields from the list of records returned. **Request** ```graphql query GetFirst5CustomerAccounts { journals( index: { name: CODE } where: { code: { like: "CUST." } } first: 5 ) { nodes { code } } } ``` **Response** ```json { "data": { "journals": { "nodes": [ { "code": "CUST.Armand" }, { "code": "CUST.Bradly" }, { "code": "CUST.Brittany" }, { "code": "CUST.Camila" }, { "code": "CUST.Carrie" } ] } } } ``` > **Note:** > > This example assumes a ledger structure where journals follow the `code` format of `CUST.`. Note also that the index used is automatically alphabetically sorted. ## Connections use PageInfo to Enable Pagination To query the nest page of records, we need to specify the **cursor position** to begin at by providing a value to the `after` argument of the query field. This tells the query to start at the cursor position and return the subsequent `n` records, where `n` is the number specified by the `first` argument. The `pageInfo` field returns a [PageInfo](/docs/reference/graphql/types/object#page-info) object with the information needed to perform this kind of cursor-based pagination. It will tell us whether there are records (pages) **before** the `startCursor` of the current page and whether there are records (pages) **after** the `endCursor`. **Request** ```graphql query GetAccountsWithPageInfo { journals( index: { name: CODE } where: { code: { like: "CUST." } } first: 5 ) { nodes { code } pageInfo { hasPreviousPage hasNextPage startCursor endCursor } } } ``` **Response** ```json { "data": { "journals": { "nodes": [ { "code": "CUST.Armand" }, { "code": "CUST.Bradly" }, { "code": "CUST.Brittany" }, { "code": "CUST.Camila" }, { "code": "CUST.Carrie" } ], "pageInfo": { "hasPreviousPage": false, "hasNextPage": true, "startCursor": "AN70l6_nkaCU5AFDVVNULkFybWFuZAABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg", "endCursor": "Ad70l6_nkaCU5AFDVVNULkNhcnJpZQABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg" } } } } ``` With this set of information, we have everything we need to write paginated queries. ## Querying with Cursors To make subsequent queries for additional pages of records, we use the cursor provided at `pageInfo.endCursor` from the current page as the `after` argument when querying the subsequent page. To simplify this procedure, let's imagine that there are 13 records that can be returned by a particular query, and we want to paginate using pages of 5 records each. We would need to perform three query operations: 1. To get the first page of 5 records, use `first: 5` while omitting the `after` argument to specify that we want to start our query at the beginning of the list. Query the `pageInfo.endCursor`. 2. To get the next page, use `first: 5` and `after: X` where `X` is the cursor returned in the first page's `pageInfo.endCursor`. 3. To get the final page, use `first: 5` and `after: Y` where `Y` is the cursor returned in the second page's `pageInfo.endCursor`. We can also render this 3-page query sequence as a table: | Page | After Cursor | Records (Nodes) | End Cursor | Next Page? | |------|--------------|-----------------|-----------------------------|------------| | `1` | `null` | `1..5` | `5b11e58c` (for record #5) | `TRUE` | | `2` | `5b11e58c` | `6..10` | `1b47d1e9` (for record #10) | `TRUE` | | `3` | `1b47d1e9` | `11..13` | `092e5914` (for record #13) | `FALSE` | Following up on the previous example using the `journals` query, we can use the `pageInfo.endCursor` from the first response as the `after` argument to get the next 5 journals: **Request** ```graphql query GetNext5Accounts { journals( index: { name: CODE } where: { code: { like: "CUST." } } first: 5 after: "Ad70l6_nkaCU5AFDVVNULkNhcnJpZQABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg" ) { nodes { code } pageInfo { hasPreviousPage hasNextPage startCursor endCursor } } } ``` **Response** ```json { "data": { "journals": { "nodes": [ { "code": "CUST.Celestino" }, { "code": "CUST.Damion" }, { "code": "CUST.Eunice" }, { "code": "CUST.Jayde" }, { "code": "CUST.Martina" } ], "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, "startCursor": "Ad70l6_nkaCU5AFDVVNULkNlbGVzdGlubwABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg", "endCursor": "Ad70l6_nkaCU5AFDVVNULk1hcnRpbmEAAQAAAP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8AASI" } } } } ``` ## Nodes and Edges Technically, every `*Connection` type returns a set of `edges` where each edge is identified by its `cursor` and contains a reference to a `node` which contains the record being queried (in this example, an [Account](/docs/reference/graphql/types/object#account) object). The `nodes` field used in the above is simply a shorthand way of accessing all the `node` fields of every edge in the `edges` list. Similarly, the `startCursor` and `endCursor` fields from `pageInfo` are just shorthand ways to access the `cursor` of the first and last edge in the `edges` list, respectively. In the example below, the `journals` query returns an [JournalConnection](/docs/reference/graphql/types/object#journal-connection) type with an `edges` field containing a list of [JournalConnectionEdge](/docs/reference/graphql/types/object#journal-connection-edge) objects. Each [JournalConnectionEdge](/docs/reference/graphql/types/object#journal-connection-edge) object has a `cursor` position and a `node` referencing the [Journal](/docs/reference/graphql/types/object#journal) record. **Request** ```graphql query GetAccountEdgesWithCursor { journals( index: { name: CODE } where: { code: { like: "CUST." } } first: 5 after: "Ad70l6_nkaCU5AFDVVNULkNhcnJpZQABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg" ) { edges { cursor node { code } } pageInfo { hasPreviousPage hasNextPage startCursor endCursor } } } ``` **Response** ```json { "data": { "journals": { "edges": [ { "cursor": "Ad70l6_nkaCU5AFDVVNULkNlbGVzdGlubwABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg", "node": { "code": "CUST.Celestino" } }, { "cursor": "Ad70l6_nkaCU5AFDVVNULkRhbWlvbgABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg", "node": { "code": "CUST.Damion" } }, { "cursor": "Ad70l6_nkaCU5AFDVVNULkV1bmljZQABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg", "node": { "code": "CUST.Eunice" } }, { "cursor": "Ad70l6_nkaCU5AFDVVNULkpheWRlAAEAAAD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AAEi", "node": { "code": "CUST.Jayde" } }, { "cursor": "Ad70l6_nkaCU5AFDVVNULk1hcnRpbmEAAQAAAP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8AASI", "node": { "code": "CUST.Martina" } } ], "pageInfo": { "hasPreviousPage": true, "hasNextPage": true, "startCursor": "Ad70l6_nkaCU5AFDVVNULkNlbGVzdGlubwABAAAA_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wABIg", "endCursor": "Ad70l6_nkaCU5AFDVVNULk1hcnRpbmEAAQAAAP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8A_wD_AP8AASI" } } } } ``` For most pagination queries, only the `nodes` field is needed. In the various examples shown in Twisp's documentation, we will usually omit `edges` and just query `nodes` to keep the response smaller and easier to read. ## Why Cursor-Based Pagination? The GraphQL core team has spent a lot more time on these questions than we have, and [they recommend opaque cursors as the best approach](https://graphql.org/learn/pagination/#pagination-and-edges). > In general, we've found that cursor-based pagination is the most powerful of those designed. There are other models out there, but for the GraphQL protocol this method provides the greatest combination of flexibility, specificity, and performance. --- # Response Format Requests to the Twisp GraphQL API return a consistent JSON response format. Source: https://www.twisp.com/docs/reference/api/response-format The API uses a response format conforming to the [GraphQL spec](https://spec.graphql.org/October2021/#sec-Response-Format). ``` { "data": { // GraphQL response } } ``` This means that every response is a JSON document with a `"data"` field. If the request produced an error, then it will also return an `"errors"` field. > The `data` entry in the response will be the result of the execution of the requested operation. If the operation was a query, this output will be an object of the query root operation type; if the operation was a mutation, this output will be an object of the mutation root operation type. > The `errors` entry in the response is a non-empty list of errors, where each error is a map. Because the Twisp API supports [Transactional Operations](/docs/reference/api/transactional-operations), if there are _any_ errors, then the entire operation is aborted and the `"data"` field will be `null`. ``` { "errors": [ // list of errors ], "data": null } ``` In addition, responses may contain an `"extensions"` field if certain request headers are specified. Learn more about extensions in the [Extensions](/docs/reference/api/extensions) reference. --- # Transactional Operations Requests to the GraphQL API are executed in a single transaction context with all-or-nothing semantics. Source: https://www.twisp.com/docs/reference/api/transactional-operations Requests may contain multiple query or mutation operations. Every operation within a single request is executed within the same database transaction context, meaning that each operation will only succeed if _all_ operations succeed. In other words, if there is an error in any single operation, _none_ of the operations will succeed. Transactional database operations are useful because they help maintain data integrity, consistency, and reliability in applications that require complex operations or involve multiple data manipulation steps. For example, the following request includes two `postTransaction` mutations, but the second uses incorrect syntax that triggers a `JSON_PARSE_ERROR`. **Request** ```graphql mutation PostTXs { tx_1: postTransaction( input: { transactionId: "5c328550-bba3-423b-a58a-b3f9786a80ad" tranCode: "ACH_CREDIT" params: { account: "260fd651-8819-4f99-9c8a-87d27e03ee4c" amount: "10.25" effective: "2022-09-08" } } ) { transactionId } tx_2: postTransaction( input: { transactionId: "cd48f439-1f7b-40aa-9b95-4fcbbb51e3cf" tranCode: "ACH_CREDIT" params: { account: "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" amount: "31.72" effective: "2022-09-09" } } ) { transactionId } } ``` **Response** ```json { "data": { "tx_1": { "transactionId": "5c328550-bba3-423b-a58a-b3f9786a80ad" }, "tx_2": { "transactionId": "cd48f439-1f7b-40aa-9b95-4fcbbb51e3cf" } } } ``` Note that the `"data"` field in the response is `null` because _neither_ transaction was posted. If we were to subsequently query for the first (valid) transaction, we would see that it was not posted. --- # CEL Function Examples Example usage of nearly all CEL functions in Twisp runtime. Source: https://www.twisp.com/docs/reference/cel/examples Twisp pervasively uses [Common Expression Language](https://cel.dev/) for computation across many apis: 1. Index Value and Filtering Definitions 2. Security Policy Conditions 3. Webhook Filters 4. Calculations 5. Velocity Controls 6. Tran Codes In addition to the standard CEL library, Twisp exposes a number of custom functions that you might find useful as you build your core accounting system. On this page you can find extensive executable examples you can use to discover what the CEL functions can do for you. ## Constructors & Conversions These functions allow you to create instances of specific data types or convert values between different types. This is essential for manipulating data within CEL expressions effectively. ### Bool Converts a string ("true" or "false") to a boolean value. ```graphql # func bool(bool) bool # func bool(string) bool mutation BoolConversion { evaluate( expressions: { fromStringTrue: "bool('true')" # true fromStringFalse: "bool('false')" # false } ) } ``` ### Bytes Converts strings or UUIDs into byte sequences. Useful for hashing or encoding functions. ```graphql # func bytes(bytes) bytes # func bytes(string) bytes # func bytes(twisp.type.v1.UUID) bytes mutation BytesConversion { evaluate( expressions: { fromString: "string(bytes('hello'))" # Should show bytes representation or convert back for check: "hello" fromUuid: "hex.EncodeToString(bytes(uuid('123e4567-e89b-12d3-a456-426614174000')))" # Hex of UUID bytes: "123e4567e89b12d3a456426614174000" } ) } ``` ### Date Constructs a Date object from a string (YYYY-MM-DD format) or a Timestamp. ```graphql # func date(twisp.type.v1.Date) twisp.type.v1.Date # func date(google.protobuf.Timestamp) twisp.type.v1.Date # func date(string) twisp.type.v1.Date mutation DateConversion { evaluate( expressions: { fromString: "string(date('2023-10-27'))" # "2023-10-27" fromTimestamp: "string(date(timestamp('2023-10-27T15:00:00Z')))" # "2023-10-27" } ) } ``` ### Decimal Constructs a high-precision Decimal object from a string representation or converts a Money object to its Decimal value. ```graphql # func decimal(twisp.type.v1.Decimal) twisp.type.v1.Decimal # func decimal(twisp.type.v1.Money) twisp.type.v1.Decimal # func decimal(string) twisp.type.v1.Decimal # func (twisp.type.v1.Money) decimal() twisp.type.v1.Decimal mutation DecimalConversion { evaluate( expressions: { fromString: "string(decimal('123.456'))" # "123.456" fromMoney: "string(decimal(money('99.99', 'USD')))" # "99.99" fromMoneyMethod: "string(money('50.00', 'EUR').decimal())" # "50.00" } ) } ``` ### Double Converts integer, unsigned integer, or string representations to a double-precision floating-point number. ```graphql # func double(double) double # func double(int) double # func double(string) double # func double(uint) double mutation DoubleConversion { evaluate( expressions: { fromInt: "double(5)" # 5.0 fromString: "double('3.14')" # 3.14 fromUint: "double(10u)" # 10.0 } ) } ``` ### Duration Constructs a Duration object from a string representation (e.g., "1h30m15s"). ```graphql # func duration(google.protobuf.Duration) google.protobuf.Duration # func duration(int) google.protobuf.Duration # func duration(string) google.protobuf.Duration mutation DurationConversion { evaluate( expressions: { fromString: "string(duration('1h30m15s'))" # "5415s" (canonical string representation) } ) } ``` ### Dyn Converts a value to a dynamic type, useful for functions accepting heterogeneous lists or for working with JSON-like structures. ```graphql # func dyn(A) dyn mutation DynExample { evaluate(expressions: { result: "dyn('hello') == dyn('hello')" }) # Example: true (demonstrates dyn conversion and comparison) } ``` ### Int Converts various types (double, duration, string, timestamp, uint) to an integer. Note that conversion from double truncates the decimal part. Timestamps convert to epoch seconds, durations to total seconds. ```graphql # func int(int) int # func int(double) int # func int(google.protobuf.Duration) int # func int(string) int # func int(google.protobuf.Timestamp) int # func int(uint) int mutation IntConversion { evaluate( expressions: { fromDouble: "int(3.9)" # 3 (truncates) fromString: "int('123')" # 123 fromUint: "int(100u)" # 100 fromTimestamp: "int(timestamp(0))" # 0 (epoch seconds) fromDuration: "int(duration('60s'))" # 60 (total seconds) } ) } ``` ### Money Constructs a Money object, typically requiring an amount (as string, decimal, or int) and a currency code string. ```graphql # func money(twisp.type.v1.Money) twisp.type.v1.Money # func money(string, string) twisp.type.v1.Money # func money(twisp.type.v1.Decimal, string) twisp.type.v1.Money # func money(int, string) twisp.type.v1.Money mutation MoneyConversion { evaluate( expressions: { fromString: "money('123.45', 'USD')" # "123.45 USD" fromDecimal: "money(decimal('99.99'), 'EUR')" # "99.99 EUR" fromInt: "money(100, 'JPY')" # "100 JPY" (no decimals for JPY often) } ) } ``` ### String Converts various data types (boolean, bytes, double, duration, int, timestamp, uint, date, decimal, UUID) into their string representation. ```graphql # func string(string) string # func string(bool) string # func string(bytes) string # func string(double) string # func string(google.protobuf.Duration) string # func string(int) string # func string(google.protobuf.Timestamp) string # func string(uint) string # func string(twisp.type.v1.Date) string # func string(twisp.type.v1.Decimal) string # func string(twisp.type.v1.UUID) string mutation StringConversion { evaluate( expressions: { fromBool: "string(true)" # "true" fromBytes: "string(b'hello')" # "hello" fromDouble: "string(3.14)" # "3.14" fromDuration: "string(duration('1m'))" # "60s" or similar fromInt: "string(123)" # "123" fromTimestamp: "string(timestamp(0))" # "1970-01-01T00:00:00Z" fromUint: "string(10u)" # "10" fromDecimal: "string(decimal('1.2'))" # "1.2" fromUuid: "string(uuid.New())" # Example: "a1b2c3d4-..." } ) } ``` ### Timestamp Constructs a Timestamp object from a string (RFC3339 format) or an integer (interpreted as epoch seconds or milliseconds, depending on implementation). ```graphql # func timestamp(google.protobuf.Timestamp) google.protobuf.Timestamp # func timestamp(int) google.protobuf.Timestamp # func timestamp(string) google.protobuf.Timestamp mutation TimestampConversion { evaluate( expressions: { fromString: "string(timestamp('2023-10-27T12:00:00Z'))" # "2023-10-27T12:00:00Z" fromEpoch: "string(timestamp(1678886400))" # Interpreted as epoch seconds: "2023-03-15T13:20:00Z" } ) } ``` ### Type Returns the type of a given value. Useful for introspection or conditional logic based on type. ```graphql # func type(A) type(A) mutation TypeExample { evaluate( expressions: { intType: "type(123) == int" # true stringType: "type('abc') == string" # true } ) } ``` ### Uint Converts double, integer, or string representations to an unsigned integer. Conversion from double truncates. ```graphql # func uint(uint) uint # func uint(double) uint # func uint(int) uint # func uint(string) uint mutation UintConversion { evaluate( expressions: { fromDouble: "uint(3.9)" # 3u (truncates) fromString: "uint('123')" # 123u fromInt: "uint(100)" # 100u } ) } ``` ### UUID Constructs a UUID object from its string or byte representation. ```graphql # func uuid(twisp.type.v1.UUID) twisp.type.v1.UUID # func uuid(string) twisp.type.v1.UUID # func uuid(bytes) twisp.type.v1.UUID mutation UuidConversion { evaluate( expressions: { fromString: "string(uuid('123e4567-e89b-12d3-a456-426614174000'))" # "123e4567-e89b-12d3-a456-426614174000" fromBytes: "string(uuid(b'\\x12>Eg\\xe8\\x9b\\x12\\xd3\\xa4VBC\\x14\\x17@\\x00'))" # "123e4567-e89b-12d3-a456-426614174000" } ) } ``` ## Operators CEL supports common operators for arithmetic, comparison, logic, and collection access. ### Arithmetic Operators #### Addition / Concatenation (+) Performs addition for numeric types (int, double, decimal, money) and duration/timestamp combinations. Concatenates strings, bytes, and lists. ```graphql # func _+_(...) (Addition/Concatenation Operator) mutation AddOperator { evaluate( expressions: { int: "5 + 3" # 8 double: "1.5 + 2.5" # 4.0 string: "'hello' + ' ' + 'world'" # "hello world" list: "[1, 2] + [3, 4]" # [1, 2, 3, 4] timestampDuration: "string(timestamp('2023-01-01T00:00:00Z') + duration('24h'))" # "2023-01-02T00:00:00Z" durationDuration: "string(duration('1h') + duration('30m'))" # "5400s" or "1h30m" decimal: "string(decimal('1.1') + decimal('2.2'))" # "3.3" } ) } ``` #### Subtraction (-) Performs subtraction for numeric types (int, double, decimal, money), duration/timestamp combinations, and between two timestamps (resulting in a duration). ```graphql # func _-_(...) (Subtraction Operator) mutation SubtractOperator { evaluate( expressions: { int: "10 - 3" # 7 double: "5.5 - 1.5" # 4.0 timestampDuration: "string(timestamp('2023-01-02T00:00:00Z') - duration('24h'))" # "2023-01-01T00:00:00Z" timestampTimestamp: "string(timestamp('2023-01-02T00:00:00Z') - timestamp('2023-01-01T00:00:00Z'))" # "86400s" or "24h" durationDuration: "string(duration('1h') - duration('15m'))" # "2700s" or "45m" decimal: "string(decimal('3.3') - decimal('1.1'))" # "2.2" } ) } ``` #### Multiplication (*) Performs multiplication for numeric types (int, uint, double, decimal). ```graphql # func _*_(double, double) double # func _*_(int, int) int # func _*_(uint, uint) uint (Multiplication Operator) mutation MultiplicationOperator { evaluate( expressions: { int: "5 * 3" # 15 uint: "5u * 3u" # 15u double: "1.5 * 2.0" # 3.0 # decimal: decimal('1.5') * decimal('2') # decimal('3.0') } ) } ``` #### Division (/) Performs division for numeric types (int, uint, double, decimal). Integer and uint division truncates towards zero. ```graphql # func _/_(double, double) double # func _/_(int, int) int # func _/_(uint, uint) uint (Division Operator) mutation DivisionOperator { evaluate( expressions: { int: "10 / 3" # 3 (integer division) uint: "10u / 3u" # 3u double: "10.0 / 4.0" # 2.5 # decimal: decimal('10') / decimal('4') # decimal('2.5') } ) } ``` #### Modulo (%) Calculates the remainder of integer or unsigned integer division. ```graphql # func _%_(int, int) int # func _%_(uint, uint) uint (Modulo Operator) mutation ModuloOperator { evaluate( expressions: { int: "10 % 3" # 1 uint: "10u % 3u" # 1u } ) } ``` #### Unary Negation (-) Negates an integer or double value. ```graphql # func -_(double) double # func -_(int) int (Unary Negation) mutation UnaryNegation { evaluate( expressions: { int: "-5" # -5 double: "-3.14" # -3.14 } ) } ``` ### Comparison Operators These operators compare two values and return a boolean result. They work across various compatible types (numbers, strings, timestamps, durations, decimals, money, bytes). #### Equality (==) Checks if two values are equal. For lists and maps, this typically performs a deep equality check. ```graphql # func _==_(A, A) bool (Equality Operator) mutation EqualityOperator { evaluate( expressions: { int: "5 == 5" # true string: "'hello' == 'hello'" # true bool: "true == !false" # true list: "[1, 2] == [1, 2]" # true (usually deep equality) } ) } ``` #### Inequality (!=) Checks if two values are not equal. ```graphql # func _!=_(A, A) bool (Inequality Operator) mutation InequalityOperator { evaluate( expressions: { int: "5 != 10" # true string: "'hello' != 'world'" # true list: "[1] != [2]" # true } ) } ``` #### Less Than (<) Checks if the left operand is strictly less than the right operand. ```graphql # func _<_(...) bool (Comparison Operator) mutation LessThanOperator { evaluate( expressions: { int: "5 < 10" # true double: "3.14 < 3.15" # true string: "'apple' < 'banana'" # true timestamp: "timestamp('2023-01-01T00:00:00Z') < timestamp('2023-01-02T00:00:00Z')" # true decimal: "decimal('1.2') < decimal('1.3')" # true } ) } ``` #### Less Than or Equal (<=) Checks if the left operand is less than or equal to the right operand. ```graphql # func _<=_(...) bool (Less Than or Equal Operator) mutation LessThanOrEqualOperator { evaluate( expressions: { int: "5 <= 5" # true double: "3.14 <= 3.14" # true string: "'apple' <= 'apple'" # true } ) } ``` #### Greater Than (>) Checks if the left operand is strictly greater than the right operand. ```graphql # func _>_(...) bool (Greater Than Operator) mutation GreaterThanOperator { evaluate( expressions: { int: "10 > 5" # true double: "3.15 > 3.14" # true string: "'banana' > 'apple'" # true decimal: "decimal('1.3') > decimal('1.2')" # true } ) } ``` #### Greater Than or Equal (>=) Checks if the left operand is greater than or equal to the right operand. ```graphql # func _>=_(...) bool (Greater Than or Equal Operator) mutation GreaterThanOrEqualOperator { evaluate( expressions: { int: "5 >= 5" # true double: "3.15 >= 3.14" # true string: "'b' >= 'a'" # true } ) } ``` ### Logical Operators #### Logical AND (&&) Returns true if both boolean operands are true, otherwise false. Short-circuits (does not evaluate the right operand if the left is false). ```graphql # func _&&_(bool, bool) bool (Logical AND) mutation LogicalAnd { evaluate(expressions: { result: "true && (1 < 2)" }) # Example: true } ``` #### Logical OR (||) Returns true if at least one boolean operand is true, otherwise false. Short-circuits (does not evaluate the right operand if the left is true). ```graphql # func _||_(bool, bool) bool (Logical OR) mutation LogicalOr { evaluate(expressions: { result: "false || (1 < 2)" }) # Example: true } ``` #### Logical NOT (!) Inverts a boolean value (true becomes false, false becomes true). ```graphql # func !_(bool) bool (Logical NOT) mutation LogicalNot { evaluate(expressions: { result: "!false" }) # Example: true } ``` ### Conditional (Ternary) Operator (?:) Evaluates a boolean condition. If true, returns the second operand; if false, returns the third operand. ```graphql # func _?_:_(bool, A, A) A mutation TernaryOperator { evaluate(expressions: { result: "true ? 'yes' : 'no'" }) # Example: "yes" } ``` ### Collection Operators #### In Operator (in) Checks for membership. For lists, it checks if an element exists. For maps, it checks if a key exists. `@in` and `_in_` are alternative syntaxes. ```graphql # func @in(A, list(A)) bool # func in(A, list(A)) bool # func _in_(A, list(A)) bool mutation InOperatorList { evaluate(expressions: { result: "2 in [1, 2, 3]" }) # Example: true } # func @in(A, map(A, B)) bool # func in(A, map(A, B)) bool # func _in_(A, map(A, B)) bool mutation InOperatorMap { evaluate(expressions: { result: "'b' in {'a': 1, 'b': 2}" }) # Example: true (checks for key existence) } ``` #### Index Operator ([]) Accesses elements in lists by integer index or values in maps by key. Also works on optional lists/maps, returning an optional value. Accessing out-of-bounds index or non-existent key results in an error unless used on an optional type. ```graphql # func _[_](list(A), int) A mutation ListIndex { evaluate(expressions: { result: "[10, 20, 30][1]" }) # Example: 20 } # func _[_](map(A, B), A) B mutation MapIndex { evaluate(expressions: { result: "{'a': 1, 'b': 2}['a']" }) # Example: 1 } # func _[_](optional_type(list(V)), int) optional_type(V) mutation OptionalListIndex { evaluate( expressions: { present: "optional.of([10, 20])[0].value()" # 10 absent: "optional.of([10, 20])[2].hasValue()" # false (index out of bounds returns empty optional) } ) } # func _[_](optional_type(map(K, V)), K) optional_type(V) mutation OptionalMapIndex { evaluate( expressions: { present: "optional.of({'a': 1})['a'].value()" # 1 absent: "optional.of({'a': 1})['b'].hasValue()" # false (key not found returns empty optional) } ) } ``` #### Safe Index Operator ([?]) Accesses elements in lists or maps like the standard index operator, but *always* returns an optional type. Returns an empty optional instead of an error for out-of-bounds indices or non-existent keys. Works on both regular and optional collections. ```graphql # func _[?_](list(V), int) optional_type(V) mutation SafeListIndexPresent { evaluate(expressions: { result: "[10, 20][?0].value()" }) # Example: 10 } mutation SafeListIndexAbsent { evaluate(expressions: { result: "[10, 20][?2].hasValue()" }) # Example: false } # func _[?_](optional_type(list(V)), int) optional_type(V) mutation SafeOptionalListIndex { evaluate( expressions: { present: "optional.of([10, 20])[?0].value()" # 10 absentIndex: "optional.of([10, 20])[?2].hasValue()" # false absentList: "optional.none()[?0].hasValue()" # false } ) } # func _[?_](map(K, V), K) optional_type(V) mutation SafeMapIndexPresent { evaluate(expressions: { result: "{'a': 1}[?'a'].value()" }) # Example: 1 } mutation SafeMapIndexAbsent { evaluate(expressions: { result: "{'a': 1}[?'b'].hasValue()" }) # Example: false } # func _[?_](optional_type(map(K, V)), K) optional_type(V) mutation SafeOptionalMapIndex { evaluate( expressions: { present: "optional.of({'a': 1})[?'a'].value()" # 1 absentKey: "optional.of({'a': 1})[?'b'].hasValue()" # false absentMap: "optional.none()[?'a'].hasValue()" # false } ) } ``` #### Optional Field Access (.?) Safely accesses fields on dynamic types (`dyn`). If the field exists, it returns an optional containing the field's value. If the field does not exist, it returns an empty optional instead of an error. ```graphql # func _?._(dyn, string) optional_type(V) mutation OptionalFieldAccess { evaluate( expressions: { present: "dyn({'a': 1}).?a.value()" # 1 absent: "dyn({'a': 1}).?b.hasValue()" # false } ) } ``` ## Optional Type Helpers Functions for creating and working with optional types, which represent values that may or may not be present. ### optional.of Creates an optional containing the given value. ```graphql # func optional.of(V) optional_type(V) mutation OptionalOf { evaluate(expressions: { result: "optional.of('value').value()" }) # Example: "value" } ``` ### optional.none Creates an empty optional (representing no value). ```graphql # func optional.none() optional_type(V) mutation OptionalNone { evaluate(expressions: { result: "optional.none().hasValue()" }) # Example: false } ``` ### optional.ofNonZeroValue Creates an optional containing the value if it's not the zero-value for its type (e.g., not 0 for int, not "" for string, not false for bool). Otherwise, creates an empty optional. ```graphql # func optional.ofNonZeroValue(V) optional_type(V) mutation OptionalOfNonZeroValueInt { evaluate( expressions: { zero: "optional.ofNonZeroValue(0).hasValue()" # false nonZero: "optional.ofNonZeroValue(5).hasValue()" # true } ) } mutation OptionalOfNonZeroValueString { evaluate( expressions: { empty: "optional.ofNonZeroValue('').hasValue()" # false nonEmpty: "optional.ofNonZeroValue('hello').hasValue()" # true } ) } ``` ### (optional) hasValue Checks if the optional contains a value (returns true) or is empty (returns false). ```graphql # func (optional_type(V)) hasValue() bool mutation OptionalHasValue { evaluate( expressions: { present: "optional.of('hello').hasValue()" # true absent: "optional.none().hasValue()" # false } ) } ``` ### (optional) value Extracts the value from the optional. **Important:** This will cause an error if the optional is empty. Use `hasValue` to check first, or use `orValue`. ```graphql # func (optional_type(V)) value() V mutation OptionalValue { evaluate(expressions: { result: "optional.of('hello').value()" }) # Example: "hello" (Errors if optional is empty) } ``` ### (optional) or Takes two optionals. Returns the first optional if it has a value, otherwise returns the second optional. ```graphql # func (optional_type(V)) or(optional_type(V)) optional_type(V) mutation OptionalOr { evaluate( expressions: { first: "optional.of(1).or(optional.of(2)).value()" # 1 second: "optional.none().or(optional.of(2)).value()" # 2 bothNone: "optional.none().or(optional.none()).hasValue()" # false } ) } ``` ### (optional) orValue Extracts the value from the optional if it's present. If the optional is empty, returns the provided default value instead. ```graphql # func (optional_type(V)) orValue(V) V mutation OptionalOrValue { evaluate( expressions: { has: "optional.of(10).orValue(0)" # 10 none: "optional.none().orValue(0)" # 0 } ) } ``` ## String Manipulation A comprehensive set of functions for working with strings, mirroring many functions from Go's `strings` package and common string methods. ### Case Conversion #### strings.ToLower Converts the entire string to lowercase, respecting Unicode rules. ```graphql # func strings.ToLower(string) string mutation StringsToLower { evaluate(expressions: { result: "strings.ToLower('HELLO WORLD')" }) # Example: "hello world" } ``` #### strings.ToUpper Converts the entire string to uppercase, respecting Unicode rules. ```graphql # func strings.ToUpper(string) string mutation StringsToUpper { evaluate(expressions: { result: "strings.ToUpper('lowercase')" }) # Example: "LOWERCASE" } ``` #### strings.ToTitle Converts the string to title case (often equivalent to uppercase for simple ASCII). Unicode rules apply. ```graphql # func strings.ToTitle(string) string mutation StringsToTitle { evaluate(expressions: { result: "strings.ToTitle('loud noises')" }) # Example: "LOUD NOISES" (Often same as ToUpper for ASCII) } ``` #### strings.Title Converts the string to title case, where the first letter of each word is capitalized. Word boundaries are Unicode-aware. ```graphql # func strings.Title(string) string # Note: Title casing rules are language specific and can be complex. mutation StringsTitle { evaluate(expressions: { result: "strings.Title('war and peace')" }) # Example: "War And Peace" (simple case) } ``` #### (string) lowerAscii Converts only ASCII characters in the string to lowercase. Faster than `strings.ToLower` but doesn't handle non-ASCII characters. ```graphql # func (string) lowerAscii() string mutation StringLowerAscii { evaluate(expressions: { result: "'HELLO WORLD 123'.lowerAscii()" }) # Example: "hello world 123" } ``` #### (string) upperAscii Converts only ASCII characters in the string to uppercase. Faster than `strings.ToUpper` but doesn't handle non-ASCII characters. ```graphql # func (string) upperAscii() string mutation StringUpperAscii { evaluate(expressions: { result: "'hello world 123'.upperAscii()" }) # Example: "HELLO WORLD 123" } ``` ### Trimming Whitespace and Characters #### strings.TrimSpace Removes leading and trailing whitespace (as defined by Unicode) from the string. ```graphql # func strings.TrimSpace(string) string mutation StringsTrimSpace { evaluate( expressions: { result: "strings.TrimSpace(' \\t\\n Hello \\n\\t ') " } ) # Example: "Hello" } ``` #### strings.Trim Removes leading and trailing characters specified in the `cutset` string. ```graphql # func strings.Trim(string, string) string mutation StringsTrim { evaluate(expressions: { result: "strings.Trim('.,!Hello!,.', '.,!')" }) # Example: "Hello" } ``` #### strings.TrimLeft Removes leading characters specified in the `cutset` string. ```graphql # func strings.TrimLeft(string, string) string mutation StringsTrimLeft { evaluate(expressions: { result: "strings.TrimLeft('...Hello...', '.')" }) # Example: "Hello..." } ``` #### strings.TrimRight Removes trailing characters specified in the `cutset` string. ```graphql # func strings.TrimRight(string, string) string mutation StringsTrimRight { evaluate(expressions: { result: "strings.TrimRight('...Hello...', '.')" }) # Example: "...Hello" } ``` #### strings.TrimPrefix Removes the specified prefix from the beginning of the string, if present. ```graphql # func strings.TrimPrefix(string, string) string mutation StringsTrimPrefix { evaluate(expressions: { result: "strings.TrimPrefix('__main__', '__')" }) # Example: "main__" } ``` #### strings.TrimSuffix Removes the specified suffix from the end of the string, if present. ```graphql # func strings.TrimSuffix(string, string) string mutation StringsTrimSuffix { evaluate(expressions: { result: "strings.TrimSuffix('filename.txt', '.txt')" }) # Example: "filename" } ``` #### (string) trim Removes leading and trailing whitespace from the string (equivalent to `strings.TrimSpace`). ```graphql # func (string) trim() string mutation StringTrim { evaluate(expressions: { result: "' whitespace '.trim()" }) # Example: "whitespace" } ``` ### Searching and Indexing #### strings.Contains Checks if the string contains the specified substring. ```graphql # func strings.Contains(string, string) bool mutation StringsContains { evaluate(expressions: { result: "strings.Contains('banana', 'nan')" }) # Example: true } ``` #### strings.ContainsAny Checks if the string contains any character from the specified `chars` string. ```graphql # func strings.ContainsAny(string, string) bool mutation StringsContainsAny { evaluate(expressions: { result: "strings.ContainsAny('team', 'eiou')" }) # Example: true ('e' and 'a' are vowels) } ``` #### strings.HasPrefix Checks if the string starts with the specified prefix. ```graphql # func strings.HasPrefix(string, string) bool mutation StringsHasPrefix { evaluate(expressions: { result: "strings.HasPrefix('__main__', '__')" }) # Example: true } ``` #### strings.HasSuffix Checks if the string ends with the specified suffix. ```graphql # func strings.HasSuffix(string, string) bool mutation StringsHasSuffix { evaluate(expressions: { result: "strings.HasSuffix('image.jpg', '.jpg')" }) # Example: true } ``` #### strings.Index Finds the index of the first occurrence of the substring within the string. Returns -1 if not found. ```graphql # func strings.Index(string, string) int mutation StringsIndex { evaluate(expressions: { result: "strings.Index('banana', 'na')" }) # Example: 2 } ``` #### strings.IndexAny Finds the index of the first occurrence of any character from the `chars` string. Returns -1 if no character is found. ```graphql # func strings.IndexAny(string, string) int mutation StringsIndexAny { evaluate(expressions: { result: "strings.IndexAny('chicken', 'aeiou')" }) # Example: 2 (index of 'i') } ``` #### strings.LastIndex Finds the index of the last occurrence of the substring within the string. Returns -1 if not found. ```graphql # func strings.LastIndex(string, string) int mutation StringsLastIndex { evaluate(expressions: { result: "strings.LastIndex('banana', 'na')" }) # Example: 4 } ``` #### strings.LastIndexAny Finds the index of the last occurrence of any character from the `chars` string. Returns -1 if no character is found. ```graphql # func strings.LastIndexAny(string, string) int mutation StringsLastIndexAny { evaluate(expressions: { result: "strings.LastIndexAny('banana', 'ab')" }) # Example: 5 ('a' at index 5) } ``` #### (string) contains Checks if the string contains the specified substring (method form). ```graphql # func (string) contains(string) bool mutation StringContains { evaluate(expressions: { result: "'banana'.contains('nan')" }) # Example: true } ``` #### (string) startsWith Checks if the string starts with the specified prefix (method form). ```graphql # func (string) startsWith(string) bool mutation StringStartsWith { evaluate(expressions: { result: "'filename.txt'.startsWith('file')" }) # Example: true } ``` #### (string) endsWith Checks if the string ends with the specified suffix (method form). ```graphql # func (string) endsWith(string) bool mutation StringEndsWith { evaluate(expressions: { result: "'image.png'.endsWith('.png')" }) # Example: true } ``` #### (string) indexOf Finds the index of the first occurrence of the substring, optionally starting the search from a given index (method form). ```graphql # func (string) indexOf(string) int mutation StringIndexOfSimple { evaluate(expressions: { result: "'banana'.indexOf('na')" }) # Example: 2 } # func (string) indexOf(string, int) int mutation StringIndexOfWithStart { evaluate(expressions: { result: "'banana'.indexOf('na', 3)" }) # Example: 4 (start search from index 3) } ``` #### (string) lastIndexOf Finds the index of the last occurrence of the substring, optionally searching backwards from a given index (method form). ```graphql # func (string) lastIndexOf(string) int mutation StringLastIndexOfSimple { evaluate(expressions: { result: "'banana'.lastIndexOf('a')" }) # Example: 5 } # func (string) lastIndexOf(string, int) int mutation StringLastIndexOfWithStart { evaluate(expressions: { result: "'banana'.lastIndexOf('a', 4)" }) # Example: 3 (search backwards from index 4) } ``` ### Replacing Substrings #### strings.Replace Replaces the first `n` occurrences of `old` with `new`. If `n` is negative, all occurrences are replaced. ```graphql # func strings.Replace(string, string, string, int) string mutation StringsReplace { evaluate(expressions: { result: "strings.Replace('banana', 'a', 'o', 2)" }) # Example: "bonona" (replace first 2 'a's) } ``` #### strings.ReplaceAll Replaces all occurrences of `old` with `new`. ```graphql # func strings.ReplaceAll(string, string, string) string mutation StringsReplaceAll { evaluate(expressions: { result: "strings.ReplaceAll('banana', 'a', 'o')" }) # Example: "bonono" } ``` #### (string) replace Replaces occurrences of `old` with `new`. If `n` is provided and non-negative, replaces the first `n` occurrences. If `n` is omitted or negative, replaces all occurrences (method form). ```graphql # func (string) replace(string, string) string mutation StringReplaceSimple { evaluate(expressions: { result: "'banana'.replace('a', 'o')" }) # Example: "bonono" (replaces all) } # func (string) replace(string, string, int) string mutation StringReplaceN { evaluate(expressions: { result: "'banana'.replace('a', 'o', 2)" }) # Example: "bonona" (replaces first 2) } ``` ### Splitting and Joining #### strings.Split Splits the string into a list of strings using the separator `sep`. ```graphql # func (string) split(string) list(string) mutation StringSplit { evaluate(expressions: { result: "'a-b-c'.split('-')" }) # Example: ["a", "b", "c"] } ``` #### strings.SplitN Splits the string by `sep` into at most `n` substrings. If `n` > 0, the last substring will contain the unsplit remainder. ```graphql # func (string) split(string, int) list(string) mutation StringSplitN { evaluate(expressions: { result: "'a-b-c'.split('-', 2)" }) # Example: ["a", "b-c"] (split into n substrings) } ``` #### (list) join Concatenates a list of strings into a single string, optionally using a separator. Default separator is empty string. ```graphql # func (list(string)) join() string mutation ListStringJoinDefault { evaluate(expressions: { result: "['a', 'b', 'c'].join()" }) # Example: "abc" } # func (list(string)) join(string) string mutation ListStringJoinSeparator { evaluate(expressions: { result: "['a', 'b', 'c'].join('-')" }) # Example: "a-b-c" } ``` ### Substrings and Characters #### (string) substring Extracts a portion of the string. With one argument `start`, takes characters from `start` to the end. With `start` and `end`, takes characters from `start` up to (but not including) `end`. ```graphql # func (string) substring(int) string mutation StringSubstringFrom { evaluate(expressions: { result: "'abcdef'.substring(2)" }) # Example: "cdef" } # func (string) substring(int, int) string mutation StringSubstringRange { evaluate(expressions: { result: "'abcdef'.substring(1, 4)" }) # Example: "bcd" (exclusive end index) } ``` #### (string) charAt Returns the character (as a string) at the specified zero-based index. ```graphql # func (string) charAt(int) string mutation StringCharAt { evaluate(expressions: { result: "'hello'.charAt(1)" }) # Example: "e" } ``` #### (string) take Returns the first `n` characters of the string. ```graphql # func (string) take(int) string mutation StringTake { evaluate(expressions: { result: "'abcdef'.take(3)" }) # Example: "abc" (first n chars) } ``` #### (string) drop Returns the string with the first `n` characters removed (equivalent to `substring(n)`). ```graphql # func (string) drop(int) string mutation StringDrop { evaluate(expressions: { result: "'abcdef'.drop(2)" }) # Example: "cdef" (same as substring(n)) } ``` ### Other String Functions #### strings.Count Counts the non-overlapping occurrences of a substring within the string. ```graphql # func strings.Count(string, string) int mutation StringsCount { evaluate(expressions: { result: "strings.Count('banana', 'a')" }) # Example: 3 } ``` #### strings.EqualFold Compares two strings using case-insensitive comparison (Unicode-aware). ```graphql # func strings.EqualFold(string, string) bool mutation StringsEqualFold { evaluate(expressions: { result: "strings.EqualFold('GoLang', 'golang')" }) # Example: true } ``` #### strings.Compare Compares two strings lexicographically. Returns -1 if s1 < s2, 0 if s1 == s2, 1 if s1 > s2. ```graphql # func strings.Compare(string, string) int mutation StringsCompare { evaluate( expressions: { less: "strings.Compare('a', 'b')" # -1 equal: "strings.Compare('a', 'a')" # 0 greater: "strings.Compare('b', 'a')" # 1 } ) } ``` #### strings.Repeat Repeats the string `count` times. ```graphql # func strings.Repeat(string, int) string mutation StringsRepeat { evaluate(expressions: { result: "strings.Repeat('=', 5)" }) # Example: "=====" } ``` #### strings.ToValidUTF8 Replaces invalid UTF-8 byte sequences in the string with the specified replacement string. ```graphql # func strings.ToValidUTF8(string, string) string mutation StringsToValidUTF8 { evaluate(expressions: { result: "strings.ToValidUTF8('abc\\xffdef', '?')" }) # Example: "abc?def" } ``` #### (string) format Formats the string using placeholders (like `%s`, `%d`) and a list of dynamic arguments. Placeholder syntax depends on implementation (often similar to `sprintf`). ```graphql # func (string) format(list(dyn)) string # Note: format specifiers like %s, %d are implementation specific. Using a generic example. mutation StringFormat { evaluate( expressions: { result: "'Hello, %s! You are %d.'.format(['World', dyn(30)])" } ) # Example: "Hello, World! You are 30." (Assuming %s, %d) } ``` #### (string) reverse Reverses the order of characters in the string. ```graphql # func (string) reverse() string mutation StringReverse { evaluate(expressions: { result: "'hello'.reverse()" }) # Example: "olleh" } ``` #### (string) quote Returns a double-quoted Go string literal representing the input string, with backslash escapes for control characters and quotes. ```graphql # func strings.quote(string) string mutation StringsQuote { evaluate(expressions: { result: "strings.quote('Hello \"World\"')" }) # Example: "\"Hello \\\"World\\\"\"" } ``` #### (string) size / size(string) Returns the number of characters (runes) in the string. ```graphql # func size(string) int / func (string) size() int mutation SizeString { evaluate(expressions: { result: "size('hello')" }) # Example: 5 } ``` ## Date & Time Functions for working with timestamps, durations, and dates. ### Timestamp Manipulation #### time.Now Returns the current timestamp. ```graphql # func time.Now() google.protobuf.Timestamp mutation TimeNow { evaluate(expressions: { result: "string(time.Now())" }) # Example: Current timestamp string like "2023-10-27T10:30:00Z" (will vary) } ``` #### (timestamp) getHours Extracts the hour (0-23) from the timestamp, optionally in a specified timezone. Default is UTC. ```graphql # func (google.protobuf.Timestamp) getHours() int mutation TimestampGetHoursUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getHours()" } ) # Example: 15 } # func (google.protobuf.Timestamp) getHours(string) int mutation TimestampGetHoursTimezone { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getHours('America/New_York')" } ) # Example: 11 } ``` #### (timestamp) getMinutes Extracts the minute (0-59) from the timestamp, optionally in a specified timezone. ```graphql # func (google.protobuf.Timestamp) getMinutes() int mutation TimestampGetMinutesUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getMinutes()" } ) # Example: 4 } # func (google.protobuf.Timestamp) getMinutes(string) int mutation TimestampGetMinutesTimezone { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getMinutes('America/New_York')" } ) # Example: 4 } ``` #### (timestamp) getSeconds Extracts the second (0-59) from the timestamp, optionally in a specified timezone (though seconds are usually timezone-independent). ```graphql # func (google.protobuf.Timestamp) getSeconds() int mutation TimestampGetSecondsUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05.123Z').getSeconds()" } ) # Example: 5 } # func (google.protobuf.Timestamp) getSeconds(string) int mutation TimestampGetSecondsTimezone { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05.123Z').getSeconds('America/New_York')" } ) # Example: 5 (seconds are usually timezone independent) } ``` #### (timestamp) getMilliseconds Extracts the millisecond (0-999) part of the timestamp. ```graphql # func (google.protobuf.Timestamp) getMilliseconds() int mutation TimestampGetMillisecondsUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05.123Z').getMilliseconds()" } ) # Example: 123 } # func (google.protobuf.Timestamp) getMilliseconds(string) int mutation TimestampGetMillisecondsTimezone { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05.123Z').getMilliseconds('America/New_York')" } ) # Example: 123 (milliseconds are timezone independent) } ``` #### (timestamp) getDayOfMonth / getDate Extracts the day of the month (1-31) from the timestamp, optionally in a specified timezone. `getDate` is an alias. ```graphql # func (google.protobuf.Timestamp) getDayOfMonth() int # func (google.protobuf.Timestamp) getDate() int (alias) mutation TimestampGetDayOfMonthUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getDayOfMonth()" } ) # Example: 27 } # func (google.protobuf.Timestamp) getDayOfMonth(string) int # func (google.protobuf.Timestamp) getDate(string) int (alias) mutation TimestampGetDayOfMonthTimezone { evaluate( expressions: { result: "timestamp('2023-11-01T02:00:00Z').getDayOfMonth('America/Los_Angeles')" } ) # Example: 31 (Oct 31st in PST) } ``` #### (timestamp) getDayOfWeek Extracts the day of the week (Sunday=0, Monday=1, ..., Saturday=6) from the timestamp, optionally in a specified timezone. ```graphql # func (google.protobuf.Timestamp) getDayOfWeek() int mutation TimestampGetDayOfWeekUTC { evaluate( expressions: { result: "timestamp('2023-10-27T00:00:00Z').getDayOfWeek()" } ) # Example: 5 (Friday) } # func (google.protobuf.Timestamp) getDayOfWeek(string) int mutation TimestampGetDayOfWeekTimezone { evaluate( expressions: { result: "timestamp('2023-10-27T00:00:00Z').getDayOfWeek('America/Los_Angeles')" } ) # Example: 4 (Thursday in PST) } ``` #### (timestamp) getDayOfYear Extracts the day of the year (1-366) from the timestamp, optionally in a specified timezone. ```graphql # func (google.protobuf.Timestamp) getDayOfYear() int mutation TimestampGetDayOfYearUTC { evaluate( expressions: { result: "timestamp('2023-10-27T00:00:00Z').getDayOfYear()" } ) # Example: 300 } # func (google.protobuf.Timestamp) getDayOfYear(string) int mutation TimestampGetDayOfYearTimezone { evaluate( expressions: { result: "timestamp('2023-01-01T02:00:00Z').getDayOfYear('America/Los_Angeles')" } ) # Example: 365 (Dec 31st of previous year in PST) } ``` #### (timestamp) getMonth Extracts the month (January=0, February=1, ..., December=11) from the timestamp, optionally in a specified timezone. **Note:** This is 0-indexed, unlike `date.getMonth()`. ```graphql # func (google.protobuf.Timestamp) getMonth() int mutation TimestampGetMonthUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getMonth()" } ) # Example: 9 (October) } # func (google.protobuf.Timestamp) getMonth(string) int mutation TimestampGetMonthTimezone { evaluate( expressions: { result: "timestamp('2023-01-01T02:00:00Z').getMonth('America/Los_Angeles')" } ) # Example: 11 (December in PST) } ``` #### (timestamp) getFullYear Extracts the full year (e.g., 2023) from the timestamp, optionally in a specified timezone. ```graphql # func (google.protobuf.Timestamp) getFullYear() int mutation TimestampGetFullYearUTC { evaluate( expressions: { result: "timestamp('2023-10-27T15:04:05Z').getFullYear()" } ) # Example: 2023 } # func (google.protobuf.Timestamp) getFullYear(string) int mutation TimestampGetFullYearTimezone { evaluate( expressions: { result: "timestamp('2023-01-01T02:00:00Z').getFullYear('America/Los_Angeles')" } ) # Example: 2022 (Dec 31st of previous year in PST) } ``` ### Duration Manipulation #### (duration) getHours Calculates the total number of full hours represented by the duration. ```graphql # func (google.protobuf.Duration) getHours() int mutation DurationGetHours { evaluate(expressions: { result: "duration('3h15m').getHours()" }) # Example: 3 } ``` #### (duration) getMinutes Calculates the total number of full minutes represented by the duration. ```graphql # func (google.protobuf.Duration) getMinutes() int mutation DurationGetMinutes { evaluate(expressions: { result: "duration('1h30m').getMinutes()" }) # Example: 90 } ``` #### (duration) getSeconds Calculates the total number of seconds represented by the duration (including fractional parts if the underlying duration has them, but returns an int). ```graphql # func (google.protobuf.Duration) getSeconds() int mutation DurationGetSeconds { evaluate(expressions: { result: "duration('3m45s').getSeconds()" }) # Example: 225 } ``` #### (duration) getMilliseconds Calculates the total number of milliseconds represented by the duration. ```graphql # func (google.protobuf.Duration) getMilliseconds() int mutation DurationGetMilliseconds { evaluate(expressions: { result: "duration('1s500ms').getMilliseconds()" }) # Example: 1500 } ``` ### Date Manipulation #### date.Today Returns the current date, optionally in a specified timezone. ```graphql # func date.Today() twisp.type.v1.Date mutation DateTodayUTC { evaluate(expressions: { result: "string(date.Today())" }) # Example: Current date string in UTC e.g., "2023-10-27" (will vary) } # func date.Today(string) twisp.type.v1.Date mutation DateTodayTimezone { evaluate(expressions: { result: "string(date.Today('America/New_York'))" }) # Example: Current date string in NY e.g., "2023-10-27" (will vary) } ``` #### (date) getDay Extracts the day of the month (1-31) from the date. ```graphql # func (twisp.type.v1.Date) getDay() int mutation DateGetDay { evaluate(expressions: { result: "date('2023-10-27').getDay()" }) # Example: 27 } ``` #### (date) getMonth Extracts the month (January=1, February=2, ..., December=12) from the date. **Note:** This is 1-indexed, unlike `timestamp.getMonth()`. ```graphql # func (twisp.type.v1.Date) getMonth() int mutation DateGetMonth { evaluate(expressions: { result: "date('2023-10-27').getMonth()" }) # Example: 10 (October) } ``` #### (date) getYear Extracts the full year (e.g., 2023) from the date. ```graphql # func (twisp.type.v1.Date) getYear() int mutation DateGetYear { evaluate(expressions: { result: "date('2023-10-27').getYear()" }) # Example: 2023 } ``` #### (date) toString Converts the Date object to its string representation (YYYY-MM-DD). ```graphql # func (twisp.type.v1.Date) toString() string mutation DateToString { evaluate(expressions: { result: "date('2023-10-27').toString()" }) # Example: "2023-10-27" } ``` ### Calendar Functions (cal) #### cal.WeekOfYear Calculates calendar-specific week information (likely week number, year). The exact map structure and behavior depends on the `int` parameters (e.g., first day of week, minimum days in first week). ```graphql # func cal.WeekOfYear(google.protobuf.Timestamp, int, int) map(, ) # Note: The markdown signature for map is incomplete. Assuming map[string]int or similar based on common patterns. # This function's exact map structure isn't defined, providing a basic call. mutation CalWeekOfYear { evaluate( expressions: { result: "cal.WeekOfYear(timestamp('2023-01-01T00:00:00Z'), 1, 1)" } ) # Result depends on implementation details (e.g., {'year': 2023, 'week': 1}) } ``` #### cal.ISOWeekOfYear Calculates the ISO 8601 week date (year and week number). The exact map structure returned depends on implementation. ```graphql # func cal.ISOWeekOfYear(google.protobuf.Timestamp) map(, ) # Map structure unclear from signature. Basic call. mutation CalISOWeekOfYear { evaluate( expressions: { result: "cal.ISOWeekOfYear(timestamp('2023-01-01T00:00:00Z'))" } ) # Result depends on implementation (e.g., {'year': 2022, 'week': 52}) } ``` #### cal.Quarter Calculates the quarter (1-4) of the year for the given timestamp. The mode `int` might define quarter boundaries (e.g., fiscal vs calendar). ```graphql # func cal.Quarter(google.protobuf.Timestamp, int) int # Mode int might define quarter boundaries. Using 1 as default (calendar). mutation CalQuarter { evaluate( expressions: { result: "cal.Quarter(timestamp('2023-07-01T00:00:00Z'), 1)" } ) # Example: 3 } ``` ## Decimal Arithmetic Functions for performing calculations with high-precision decimal numbers. These are crucial for financial calculations where floating-point inaccuracies are unacceptable. ### Basic Arithmetic #### decimal.Add Adds two decimal numbers. ```graphql # func decimal.Add(twisp.type.v1.Decimal, twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalAdd { evaluate( expressions: { result: "string(decimal.Add(decimal('1.1'), decimal('2.2')))" } ) # Example: "3.3" } ``` #### decimal.Sub Subtracts the second decimal from the first. ```graphql # func decimal.Sub(twisp.type.v1.Decimal, twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalSub { evaluate( expressions: { result: "string(decimal.Sub(decimal('1.23'), decimal('0.23')))" } ) # Example: "1" } ``` #### decimal.Mul Multiplies two decimal numbers. ```graphql # func decimal.Mul(twisp.type.v1.Decimal, twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalMul { evaluate( expressions: { result: "string(decimal.Mul(decimal('1.5'), decimal('2')))" } ) # Example: "3" or "3.0" } ``` #### decimal.Quo Divides the first decimal by the second, up to a specified precision (`uint`). The exact division rules (rounding) depend on implementation context. ```graphql # func decimal.Quo(twisp.type.v1.Decimal, twisp.type.v1.Decimal, uint) twisp.type.v1.Decimal mutation DecimalQuo { evaluate( expressions: { result: "string(decimal.Quo(decimal('10'), decimal('3'), 4u))" } ) # Example: "3.3333" (precision 4) } ``` #### decimal.QuoInteger Performs integer division (truncates the result) between two decimals. The `uint` likely relates to precision context but might not directly affect the truncated result. ```graphql # func decimal.QuoInteger(twisp.type.v1.Decimal, twisp.type.v1.Decimal, uint) twisp.type.v1.Decimal mutation DecimalQuoInteger { evaluate( expressions: { result: "string(decimal.QuoInteger(decimal('10'), decimal('3'), 8u))" } ) # Example: "3" } ``` #### decimal.Rem Calculates the remainder of the division between two decimals. The `uint` might specify rounding mode for the remainder calculation. ```graphql # func decimal.Rem(twisp.type.v1.Decimal, twisp.type.v1.Decimal, uint) twisp.type.v1.Decimal # Mode uint might specify rounding for remainder. Using 0 as default. mutation DecimalRem { evaluate( expressions: { result: "string(decimal.Rem(decimal('10.5'), decimal('3'), 0u))" } ) # Example: "1.5" } ``` ### Exponents, Roots, and Logs #### decimal.Pow Raises the first decimal to the power of the second decimal. ```graphql # func decimal.Pow(twisp.type.v1.Decimal, twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalPow { evaluate( expressions: { result: "string(decimal.Pow(decimal('2'), decimal('3')))" } ) # Example: "8" } ``` #### decimal.Exp Raises the decimal to the power of an unsigned integer exponent. ```graphql # func decimal.Exp(twisp.type.v1.Decimal, uint) twisp.type.v1.Decimal mutation DecimalExp { evaluate(expressions: { result: "string(decimal.Exp(decimal('1.23'), 2u))" }) # Example: "1.5129" (1.23^2) } ``` #### decimal.Sqrt Calculates the square root of the decimal. ```graphql # func decimal.Sqrt(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalSqrt { evaluate(expressions: { result: "string(decimal.Sqrt(decimal('9')))" }) # Example: "3" } ``` #### decimal.Cbrt Calculates the cube root of the decimal. ```graphql # func decimal.Cbrt(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalCbrt { evaluate(expressions: { result: "string(decimal.Cbrt(decimal('27')))" }) # Example: "3" } ``` #### decimal.Ln Calculates the natural logarithm (base e) of the decimal. ```graphql # func decimal.Ln(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalLn { evaluate(expressions: { result: "string(decimal.Ln(decimal('10')))" }) # Example: Natural log of 10 (approx "2.302585...") } ``` #### decimal.Log10 Calculates the base-10 logarithm of the decimal. ```graphql # func decimal.Log10(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalLog10 { evaluate(expressions: { result: "string(decimal.Log10(decimal('100')))" }) # Example: "2" } ``` ### Rounding and Precision #### decimal.Round Rounds the decimal to a specified number of places (`int`) using a named rounding mode (`string`, e.g., "half_up", "half_even"). ```graphql # func decimal.Round(twisp.type.v1.Decimal, string, int) twisp.type.v1.Decimal # Rounding modes might include "half_up", "half_even", etc. mutation DecimalRound { evaluate( expressions: { result: "string(decimal.Round(decimal('1.2345'), 'half_up', 2))" } ) # Example: "1.23" } ``` #### decimal.Quantize Rounds the decimal to have the same exponent (scale) as a specified number of places (`int`). Requires a rounding mode (`uint`, check implementation for specific mode values, e.g., 4=HALF_UP). This effectively sets the number of decimal places. ```graphql # func decimal.Quantize(twisp.type.v1.Decimal, int, uint) twisp.type.v1.Decimal # e.g. exponent -2 precision of 3. mutation DecimalQuantize { evaluate( expressions: { result: "string(decimal.Quantize(decimal('1.236'), -2, 3u))" } ) # Example: "1.24" } ``` #### decimal.Ceil Rounds the decimal up to the nearest integer. ```graphql # func decimal.Ceil(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalCeil { evaluate(expressions: { result: "string(decimal.Ceil(decimal('1.23')))" }) # Example: "2" } ``` #### decimal.Reduce Removes trailing zeros from the fractional part of the decimal without changing its value. ```graphql # func decimal.Reduce(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalReduce { evaluate(expressions: { result: "string(decimal.Reduce(decimal('1.2300')))" }) # Example: "1.23" } ``` ### Comparison and Sign #### decimal.Cmp Compares two decimals. Returns a decimal value: -1 if d1 < d2, 0 if d1 == d2, 1 if d1 > d2. ```graphql # func decimal.Cmp(twisp.type.v1.Decimal, twisp.type.v1.Decimal) twisp.type.v1.Decimal # Returns -1, 0, or 1 as Decimal mutation DecimalCmp { evaluate( expressions: { less: "string(decimal.Cmp(decimal('1'), decimal('2')))" # "-1" equal: "string(decimal.Cmp(decimal('2'), decimal('2')))" # "0" greater: "string(decimal.Cmp(decimal('3'), decimal('2')))" # "1" } ) } ``` #### decimal.Abs Returns the absolute value of the decimal. ```graphql # func decimal.Abs(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalAbs { evaluate(expressions: { result: "string(decimal.Abs(decimal('-1.23')))" }) # Example: "1.23" } ``` #### decimal.Neg Returns the negation of the decimal. ```graphql # func decimal.Neg(twisp.type.v1.Decimal) twisp.type.v1.Decimal mutation DecimalNeg { evaluate(expressions: { result: "string(decimal.Neg(decimal('1.23')))" }) # Example: "-1.23" } ``` ### Conversion #### (decimal) toString Converts the Decimal object to its string representation. ```graphql # func (twisp.type.v1.Decimal) toString() string mutation DecimalToString { evaluate(expressions: { result: "decimal('123.456').toString()" }) # Example: "123.456" } ``` ## Money Arithmetic Functions for performing calculations with Money objects, ensuring currency consistency. ### money.Add Adds two Money objects. Requires both operands to have the same currency. ```graphql # func money.Add(twisp.type.v1.Money, twisp.type.v1.Money) twisp.type.v1.Money mutation MoneyAdd { evaluate( expressions: { result: "money.Add(money('10', 'USD'), money('5.50', 'USD'))" } ) # Example: "15.50 USD" } ``` ### money.Sub Subtracts the second Money object from the first. Requires both operands to have the same currency. ```graphql # func money.Sub(twisp.type.v1.Money, twisp.type.v1.Money) twisp.type.v1.Money mutation MoneySub { evaluate( expressions: { result: "money.Sub(money('10.00', 'USD'), money('5.50', 'USD'))" } ) # Example: "4.50 USD" } ``` ### money.Mul Multiplies a Money object by a scalar factor (provided as a string representing a decimal). ```graphql # func money.Mul(twisp.type.v1.Money, string) twisp.type.v1.Money mutation MoneyMul { evaluate( expressions: { result: "money.Mul(money('10.50', 'USD'), '2')" } ) # Example: "21.00 USD" } ``` ### money.Div Divides a Money object by a scalar factor (provided as a string representing a decimal). ```graphql # func money.Div(twisp.type.v1.Money, string) twisp.type.v1.Money mutation MoneyDiv { evaluate( expressions: { result: "money.Div(money('21.00', 'USD'), '2')" } ) # Example: "10.50 USD" } ``` ### (money) currency Returns the currency code of the Money object as a string. ```graphql # func (twisp.type.v1.Money) currency() string mutation MoneyCurrency { evaluate(expressions: { result: "money('10.00', 'EUR').currency()" }) # Example: "EUR" } ``` ### (money) decimal Converts the Money object to its Decimal value (amount only, currency is discarded). ```graphql # func (twisp.type.v1.Money) decimal() twisp.type.v1.Decimal mutation MoneyDecimalConversion { evaluate(expressions: { fromMoneyMethod: "string(money('50.00', 'EUR').decimal())" # "50.00" }) } ``` ## Math Functions Standard mathematical functions operating primarily on `double` values. These mirror functions from Go's `math` package. ### Basic Operations #### math.Abs Returns the absolute value of a double. ```graphql # func math.Abs(double) double mutation MathAbs { evaluate(expressions: { result: "math.Abs(-5.5)" }) # Example: 5.5 } ``` #### math.Max Returns the larger of two double values. ```graphql # func math.Max(double, double) double mutation MathMax { evaluate(expressions: { result: "math.Max(5.0, 3.0)" }) # Example: 5.0 } ``` #### math.Min Returns the smaller of two double values. ```graphql # func math.Min(double, double) double mutation MathMin { evaluate(expressions: { result: "math.Min(5.0, 3.0)" }) # Example: 3.0 } ``` #### math.Dim Returns the maximum of `x-y` or `0`. (Positive difference). ```graphql # func math.Dim(double, double) double mutation MathDim { evaluate( expressions: { pos: "math.Dim(5.0, 2.0)" # 3.0 (x-y) neg: "math.Dim(2.0, 5.0)" # 0.0 (max(x-y, 0)) } ) } ``` #### math.Copysign Returns a value with the magnitude of the first argument and the sign of the second argument. ```graphql # func math.Copysign(double, double) double mutation MathCopysign { evaluate(expressions: { result: "math.Copysign(5.0, -1.0)" }) # Example: -5.0 } ``` #### math.Signbit Reports whether the number is negative or negative zero. ```graphql # func math.Signbit(double) bool mutation MathSignbit { evaluate( expressions: { neg: "math.Signbit(-5.0)" # true pos: "math.Signbit(5.0)" # false } ) } ``` ### Rounding and Truncation #### math.Ceil Returns the least integer value greater than or equal to the input. ```graphql # func math.Ceil(double) double mutation MathCeil { evaluate(expressions: { result: "math.Ceil(3.1)" }) # Example: 4.0 } ``` #### math.Floor Returns the greatest integer value less than or equal to the input. ```graphql # func math.Floor(double) double mutation MathFloor { evaluate(expressions: { result: "math.Floor(3.9)" }) # Example: 3.0 } ``` #### math.Round Returns the nearest integer, rounding half away from zero. ```graphql # func math.Round(double) double mutation MathRound { evaluate(expressions: { result: "math.Round(3.5)" }) # Example: 4.0 } ``` #### math.RoundToEven Returns the nearest integer, rounding ties to the nearest even integer (Banker's rounding). ```graphql # func math.RoundToEven(double) double mutation MathRoundToEven { evaluate( expressions: { halfUp: "math.RoundToEven(2.5)" # 2.0 (rounds to nearest even integer) halfDown: "math.RoundToEven(3.5)" # 4.0 } ) } ``` #### math.Trunc Returns the integer part of the double (truncates towards zero). ```graphql # func math.Trunc(double) double mutation MathTrunc { evaluate(expressions: { result: "math.Trunc(3.14159)" }) # Example: 3.0 } ``` ### Powers, Roots, and Logs #### math.Pow Returns x**y, the base-x exponential of y. ```graphql # func math.Pow(double, double) double mutation MathPow { evaluate(expressions: { result: "math.Pow(2.0, 3.0)" }) # Example: 8.0 } ``` #### math.Pow10 Returns 10**n, the base-10 exponential of n (n is int). ```graphql # func math.Pow10(int) double mutation MathPow10 { evaluate(expressions: { result: "math.Pow10(3)" }) # Example: 1000.0 } ``` #### math.Sqrt Returns the square root of x. ```graphql # func math.Sqrt(double) double mutation MathSqrt { evaluate(expressions: { result: "math.Sqrt(9.0)" }) # Example: 3.0 } ``` #### math.Cbrt Returns the cube root of x. ```graphql # func math.Cbrt(double) double mutation MathCbrt { evaluate(expressions: { result: "math.Cbrt(27.0)" }) # Example: 3.0 } ``` #### math.Log Returns the natural logarithm (base e) of x. ```graphql # func math.Log(double) double mutation MathLog { evaluate(expressions: { result: "math.Log(10.0)" }) # Example: ~2.302585 (natural log) } ``` #### math.Log10 Returns the base-10 logarithm of x. ```graphql # func math.Log10(double) double mutation MathLog10 { evaluate(expressions: { result: "math.Log10(100.0)" }) # Example: 2.0 } ``` #### math.Log2 Returns the base-2 logarithm of x. ```graphql # func math.Log2(double) double mutation MathLog2 { evaluate(expressions: { result: "math.Log2(8.0)" }) # Example: 3.0 } ``` #### math.Log1p Returns the natural logarithm of 1+x. More accurate than `Log(1+x)` for small x. ```graphql # func math.Log1p(double) double mutation MathLog1p { evaluate(expressions: { result: "math.Log1p(0.0)" }) # Example: 0.0 (ln(1+0)) } ``` #### math.Exp Returns e**x, the base-e exponential of x. ```graphql # func math.Exp(double) double mutation MathExp { evaluate(expressions: { result: "math.Exp(1.0)" }) # Example: ~2.71828 (e^1) } ``` #### math.Exp2 Returns 2**x, the base-2 exponential of x. ```graphql # func math.Exp2(double) double mutation MathExp2 { evaluate(expressions: { result: "math.Exp2(3.0)" }) # Example: 8.0 (2^3) } ``` #### math.Expm1 Returns e**x - 1. More accurate than `Exp(x) - 1` for small x. ```graphql # func math.Expm1(double) double mutation MathExpm1 { evaluate(expressions: { result: "math.Expm1(0.0)" }) # Example: 0.0 (exp(0)-1) } ``` ### Trigonometry and Geometry #### math.Hypot Returns Sqrt(p*p + q*q), the length of the hypotenuse of a right triangle with legs p and q. ```graphql # func math.Hypot(double, double) double mutation MathHypot { evaluate(expressions: { result: "math.Hypot(3.0, 4.0)" }) # Example: 5.0 } ``` ### Other Math Functions #### math.Mod Returns the floating-point remainder of x/y. The result has the same sign as x. ```graphql # func math.Mod(double, double) double mutation MathMod { evaluate(expressions: { result: "math.Mod(10.0, 3.0)" }) # Example: 1.0 } ``` #### math.Remainder Returns the IEEE 754 floating-point remainder of x/y. ```graphql # func math.Remainder(double, double) double mutation MathRemainder { evaluate(expressions: { result: "math.Remainder(10.5, 3.0)" }) # Example: 1.5 } ``` #### math.FMA Returns x * y + z, computed with only one rounding. (Fused Multiply-Add). ```graphql # func math.FMA(double, double, double) double mutation MathFMA { evaluate(expressions: { result: "math.FMA(2.0, 3.0, 4.0)" }) # Example: 10.0 (2*3 + 4) } ``` ## Random Number Generation (rand) Functions for generating pseudo-random numbers, based on Go's `math/rand`. ### Integers #### rand.Int Returns a non-negative pseudo-random 64-bit integer. ```graphql # func rand.Int() int mutation RandInt { evaluate(expressions: { result: "rand.Int()" }) # Example: A random int64 value (e.g., 1234567890123456789) } ``` #### rand.Int31 Returns a non-negative pseudo-random 31-bit integer as an int. ```graphql # func rand.Int31() int mutation RandInt31 { evaluate(expressions: { result: "rand.Int31()" }) # Example: Random non-negative int32 (e.g., 1073741823) } ``` #### rand.Int63 Returns a non-negative pseudo-random 63-bit integer as an int. ```graphql # func rand.Int63() int mutation RandInt63 { evaluate(expressions: { result: "rand.Int63()" }) # Example: Random non-negative int64 (e.g., 4611686018427387903) } ``` #### rand.Intn Returns a non-negative pseudo-random integer in the range [0, n). Panics if n <= 0. ```graphql # func rand.Intn(int) int mutation RandIntn { evaluate(expressions: { result: "rand.Intn(100)" }) # Example: Random int between 0 and 99 } ``` #### rand.Int31n Returns a non-negative pseudo-random 31-bit integer in the range [0, n). Panics if n <= 0. ```graphql # func rand.Int31n(int) int mutation RandInt31n { evaluate(expressions: { result: "rand.Int31n(1000)" }) # Random int32 between 0 and 999 } ``` #### rand.Int63n Returns a non-negative pseudo-random 63-bit integer in the range [0, n). Panics if n <= 0. ```graphql # func rand.Int63n(int) int mutation RandInt63n { evaluate(expressions: { result: "rand.Int63n(100)" }) # Example: A random int64 between 0 and 99 } ``` ### Unsigned Integers #### rand.Uint32 Returns a pseudo-random 32-bit value as a uint. ```graphql # func rand.Uint32() uint mutation RandUint32 { evaluate(expressions: { result: "rand.Uint32()" }) # Example: Random uint32 (e.g., 2147483647u) } ``` #### rand.Uint64 Returns a pseudo-random 64-bit value as a uint. ```graphql # func rand.Uint64() uint mutation RandUint64 { evaluate(expressions: { result: "rand.Uint64()" }) # Example: A random uint64 value (e.g., 9223372036854775807u) } ``` ### Floating-Point Numbers #### rand.Float32 Returns a pseudo-random float32 in [0.0, 1.0). Returned as a double. ```graphql # func rand.Float32() double mutation RandFloat32 { evaluate(expressions: { result: "rand.Float32()" }) # Example: Random float32 (returned as double) between 0.0 and 1.0 (e.g., 0.1234567) } ``` #### rand.Float64 Returns a pseudo-random float64 in [0.0, 1.0). ```graphql # func rand.Float64() double mutation RandFloat64 { evaluate(expressions: { result: "rand.Float64()" }) # Example: Random float64 between 0.0 and 1.0 (e.g., 0.987654321) } ``` #### rand.NormFloat64 Returns a normally distributed float64 value with mean 0 and standard deviation 1. ```graphql # func rand.NormFloat64() double mutation RandNormFloat64 { evaluate(expressions: { result: "rand.NormFloat64()" }) # Example: Random normally distributed float64 (e.g., -0.54321) } ``` #### rand.ExpFloat64 Returns an exponentially distributed float64 value with rate parameter 1. ```graphql # func rand.ExpFloat64() double mutation RandExpFloat64 { evaluate(expressions: { result: "rand.ExpFloat64()" }) # Example: Random exponentially distributed float64 (e.g., 1.2345) } ``` ## Hashing & Checksums Functions for calculating various hash digests. These typically operate on `bytes`. ### MD5 Calculates the MD5 hash (128-bit). **Note:** MD5 is cryptographically broken and should not be used for security purposes. ```graphql # func md5.Sum(bytes) bytes mutation Md5Sum { evaluate(expressions: { result: "hex.EncodeToString(md5.Sum(b'hello'))" }) # Example: "5d41402abc4b2a76b9719d911017c592" } ``` ### SHA-1 Calculates the SHA-1 hash (160-bit). **Note:** SHA-1 is also considered cryptographically weak and should be avoided for security contexts. ```graphql # func sha1.Sum(bytes) bytes mutation Sha1Sum { evaluate(expressions: { result: "hex.EncodeToString(sha1.Sum(b'hello'))" }) # Example: "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d" } ``` ### SHA-256 Calculates the SHA-256 hash (256-bit). A standard secure hashing algorithm. ```graphql # func sha256.Sum256(bytes) bytes mutation Sha256Sum256 { evaluate( expressions: { result: "hex.EncodeToString(sha256.Sum256(b'hello'))" } ) # Example: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" } ``` ### SHA-512 Calculates the SHA-512 hash (512-bit). A standard secure hashing algorithm. ```graphql # func sha512.Sum512(bytes) bytes mutation Sha512Sum512 { evaluate( expressions: { result: "hex.EncodeToString(sha512.Sum512(b'hello'))" } ) # Example: "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" } ``` ### System Hashes #### system.hash.xx Calculates a non-cryptographic xxHash64 hash, returning a `uint`. Useful for fast hashing where collision resistance is less critical than speed. ```graphql # func system.hash.xx(bytes) uint mutation SystemHashXx { evaluate(expressions: { result: "system.hash.xx(b'data')" }) # Example: xxHash64 uint result (e.g., 17241709254074915385u) } ``` #### system.hash.jump Implements the Jump Consistent Hash algorithm. Given a `uint` key and an `int` number of buckets, returns a bucket index (`int` from 0 to num_buckets-1) consistently assigned to that key. Useful for distributing items across shards/buckets. ```graphql # func system.hash.jump(uint, int) int mutation SystemHashJump { evaluate(expressions: { result: "system.hash.jump(123456789u, 10)" }) # Example: Jump consistent hash bucket index (0-9) (e.g., 7) } ``` ## Encoding & Decoding Functions for converting data between different representations (e.g., bytes to hex, base64). ### Hexadecimal #### hex.EncodeToString Encodes a byte sequence into its hexadecimal string representation. ```graphql # func hex.EncodeToString(bytes) string mutation HexEncodeToString { evaluate( expressions: { result: "hex.EncodeToString(b'\\xDE\\xAD\\xBE\\xEF')" } ) # Example: "deadbeef" } ``` #### hex.DecodeString Decodes a hexadecimal string representation back into a byte sequence. ```graphql # func hex.DecodeString(string) bytes mutation HexDecodeString { evaluate(expressions: { result: "string(hex.DecodeString('68656c6c6f'))" }) # Example: "hello" (bytes converted back to string for display) } ``` ### Base64 #### base64.encode Encodes a byte sequence into its Base64 string representation (standard encoding). ```graphql # func base64.encode(bytes) string mutation Base64Encode { evaluate(expressions: { result: "base64.encode(b'hello world')" }) # Example: "aGVsbG8gd29ybGQ=" } ``` #### base64.decode Decodes a Base64 string representation back into a byte sequence. ```graphql # func base64.decode(string) bytes mutation Base64Decode { evaluate(expressions: { result: "string(base64.decode('aGVsbG8gd29ybGQ='))" }) # Example: "hello world" (bytes converted back to string) } ``` ### HTML Escaping #### html.EscapeString Escapes special HTML characters (`<`, `>`, `&`, `"`, `'`) into their corresponding HTML entities. Useful for preventing XSS when embedding strings in HTML. ```graphql # func html.EscapeString(string) string mutation HtmlEscapeString { evaluate( expressions: { result: "html.EscapeString('')" } ) # Example: "<script>alert("XSS")</script>" } ``` #### html.UnescapeString Unescapes HTML entities (like `<`, `&`, `"`) back into their original characters. ```graphql # func html.UnescapeString(string) string mutation HtmlUnescapeString { evaluate( expressions: { result: "html.UnescapeString('<tag> & "quote"')" } ) # Example: " & \"quote\"" } ``` ### URL Escaping #### url.PathEscape Escapes a string so it can be safely placed in a URL path segment. Spaces become `%20`, `/` remains unescaped. ```graphql # func url.PathEscape(string) string mutation UrlPathEscape { evaluate(expressions: { result: "url.PathEscape('a b/c?d=e')" }) # Example: "a%20b/c%3Fd=e" } ``` #### url.QueryEscape Escapes a string so it can be safely placed in a URL query parameter value. Spaces become `+` (or `%20` depending on context/implementation). ```graphql # func url.QueryEscape(string) string mutation UrlQueryEscape { evaluate( expressions: { result: "url.QueryEscape('key=value&another key=a/b')" } ) # Example: "key%3Dvalue%26another+key%3Da%2Fb" } ``` ### JSON Marshaling #### json.Marshal Converts a CEL value (often a map or list, represented as `dyn` or `google.protobuf.Any`) into its JSON byte representation. ```graphql # func json.Marshal(google.protobuf.Any) bytes # Requires a proto Any type, harder to demo simply. Example assumes a map can be marshaled. mutation JsonMarshal { evaluate(expressions: { result: "string(json.Marshal({'key': 'value'}))" }) # Example: "{'key': 'value'}" (or similar JSON bytes as string) } ``` ## Path Manipulation (path) Functions for working with slash-separated file paths, mirroring Go's `path` package. ### path.Base Returns the last element of the path (the filename or directory name). ```graphql # func path.Base(string) string mutation PathBase { evaluate(expressions: { result: "path.Base('/usr/local/file.txt')" }) # Example: "file.txt" } ``` ### path.Clean Returns the shortest path name equivalent to the path by purely lexical processing. It applies the following rules iteratively until no further processing can be done: 1. Replace multiple slashes with a single slash. 2. Eliminate each `.` path name element (the current directory). 3. Eliminate each inner `..` path name element (the parent directory) along with the preceding non-`..` element. 4. Eliminate `..` elements that begin a rooted path: that is, replace `/..` by `/` at the beginning of a path. ```graphql # func path.Clean(string) string mutation PathClean { evaluate(expressions: { result: "path.Clean('/a/b/../c//d')" }) # Example: "/a/c/d" } ``` ### path.Dir Returns all but the last element of the path, typically the path's directory. ```graphql # func path.Dir(string) string mutation PathDir { evaluate(expressions: { result: "path.Dir('/usr/local/bin')" }) # Example: "/usr/local" } ``` ### path.Ext Returns the file name extension used by path. The extension is the suffix beginning at the final dot in the final element of path; it is empty if there is no dot. ```graphql # func path.Ext(string) string mutation PathExt { evaluate(expressions: { result: "path.Ext('archive.tar.gz')" }) # Example: ".gz" } ``` ### path.IsAbs Reports whether the path is absolute (begins with `/`). ```graphql # func path.IsAbs(string) bool mutation PathIsAbs { evaluate( expressions: { absolute: "path.IsAbs('/home/user')" # true relative: "path.IsAbs('data/file')" # false } ) } ``` ## UUID Generation & Manipulation (uuid) Functions specifically for creating UUIDs. ### uuid.New Generates a new Version 4 (random) UUID. ```graphql # func uuid.New() twisp.type.v1.UUID mutation UuidNew { evaluate(expressions: { result: "string(uuid.New())" }) # Example: A new Version 4 UUID string (e.g., "f47ac10b-58cc-4372-a567-0e02b2c3d479") } ``` ### uuid.NewMD5 Generates a Version 3 (MD5 hash-based) UUID from a namespace UUID and a name (bytes). ```graphql # func uuid.NewMD5(twisp.type.v1.UUID, bytes) twisp.type.v1.UUID mutation UuidNewMD5 { evaluate(expressions: { result: "string(uuid.NewMD5(uuid.New(), b'data'))" }) # Example: MD5-based UUID string (e.g., "a1b...") } ``` ### uuid.NewSHA1 Generates a Version 5 (SHA-1 hash-based) UUID from a namespace UUID and a name (bytes). ```graphql # func uuid.NewSHA1(twisp.type.v1.UUID, bytes) twisp.type.v1.UUID mutation UuidNewSHA1 { evaluate(expressions: { result: "string(uuid.NewSHA1(uuid.New(), b'data'))" }) # Example: SHA1-based UUID string (e.g., "c2d...") } ``` ### (uuid) toString Converts the UUID object to its standard string representation. ```graphql # func (twisp.type.v1.UUID) toString() string mutation UuidToString { evaluate( expressions: { result: "uuid('123e4567-e89b-12d3-a456-426614174000').toString()" } ) # Example: "123e4567-e89b-12d3-a456-426614174000" } ``` ## Finance Functions (finance) Specialized functions for financial calculations. Examples provided are basic calls; understanding the financial formulas is necessary for correct usage. *Note: The exact signatures and behavior might vary.* ### finance.PrincipalPayment Calculates the principal portion of a loan payment for a given period. Arguments typically include rate per period, payment period number, total number of periods, present value (loan amount), future value, and payment type (0 for end of period, 1 for beginning). ```graphql # func finance.PrincipalPayment(double, int, int, double, double, int) double mutation FinancePrincipalPayment { evaluate( expressions: { # Example: Principal for period 1 of a $200k, 30yr (360mo) loan at 5% APR (0.05/12 monthly) result: "finance.PrincipalPayment(0.05/12.0, 1, 360, 200000.0, 0.0, 0)" } ) # Example financial calculation result (e.g., ~239.98) } ``` ### finance.InterestPayment Calculates the interest portion of a loan payment for a given period. Arguments are similar to `PrincipalPayment`. ```graphql # func finance.InterestPayment(double, int, int, double, double, int) double mutation FinanceInterestPayment { evaluate( expressions: { # Example: Interest for period 1 of a $200k, 30yr (360mo) loan at 5% APR (0.05/12 monthly) result: "finance.InterestPayment(0.05/12.0, 1, 360, 200000.0, 0.0, 0)" } ) # Example financial calculation result (e.g., ~833.33) } ``` ### finance.Payment Calculates the total payment (principal + interest) per period for a loan or annuity. Arguments include rate per period, number of periods, present value, future value, and payment type. ```graphql # func finance.Payment(double, int, double, double, int) double mutation FinancePayment { evaluate( expressions: { # Example: Total payment for a $200k, 30yr (360mo) loan at 5% APR (0.05/12 monthly) result: "finance.Payment(0.05/12.0, 360, 200000.0, 0.0, 0)" } ) # Example financial calculation result (e.g., ~1073.31) } ``` *(Other finance functions like Rate, PresentValue, FutureValue, Depreciation, etc., would follow a similar pattern with example calls based on their expected arguments)* ## System & Utility Functions Miscellaneous functions for tasks like printing, type checking, and collection manipulation. ### print Outputs the provided message and value to the system logs (side-effect) and returns the value itself. Useful for debugging CEL expressions. ```graphql # func print(string, google.protobuf.Any) google.protobuf.Any # Print is for side-effects (logging), returns its second argument. mutation PrintExample { evaluate(expressions: { result: "print('Input value:', 123)" }) # Example: 123 (and logs 'Input value: 123' server-side) } ``` ### system.mergeMap Merges two maps (represented as `dyn`). Keys from the second map overwrite keys in the first map if they overlap. ```graphql # func system.mergeMap(dyn, dyn) dyn mutation SystemMergeMap { # Merges second map into first, overwriting common keys evaluate( expressions: { result: "system.mergeMap({'a': 1, 'b': 2}, {'b': 3, 'c': 4})" } ) # Example: {'a': 1, 'b': 3, 'c': 4} } ``` ### system.unique Removes duplicate UUIDs from a list of UUIDs. The order of the remaining unique elements is not guaranteed. ```graphql # func system.unique(list(twisp.type.v1.UUID)) list(twisp.type.v1.UUID) mutation SystemUnique { evaluate( expressions: { # Constructing UUIDs inline for example ids: "[uuid('11111111-1111-1111-1111-111111111111'), uuid('22222222-2222-2222-2222-222222222222'), uuid('11111111-1111-1111-1111-111111111111')]" uniqueIds: "system.unique([uuid('11111111-1111-1111-1111-111111111111'), uuid('22222222-2222-2222-2222-222222222222'), uuid('11111111-1111-1111-1111-111111111111')])" # uniqueIds result should be the first two UUIDs (order might vary) # e.g., [uuid('11111111-1111-1111-1111-111111111111'), uuid('22222222-2222-2222-2222-222222222222')] } ) } ``` ### Size Functions Calculates the size (number of elements or characters) of various types. ```graphql # func size(bytes) int / func (bytes) size() int mutation SizeBytes { evaluate(expressions: { result: "size(b'hello')" }) # Example: 5 } # func size(list(A)) int / func (list(A)) size() int mutation SizeList { evaluate(expressions: { result: "size([1, 2, 3])" }) # Example: 3 } # func size(map(A, B)) int / func (map(A, B)) size() int mutation SizeMap { evaluate(expressions: { result: "size({'a': 1, 'b': 2})" }) # Example: 2 } # func size(string) int / func (string) size() int mutation SizeString { evaluate(expressions: { result: "size('hello')" }) # Example: 5 } ``` ### Regex Matching #### matches Performs a regular expression match using RE2 syntax. Can be used as a global function or a method on strings. ```graphql # func matches(string, string) bool # This is a global function, often used as receiver method. mutation GlobalMatches { evaluate(expressions: { result: "matches('abc', '^a.*c$')" }) # Example: true } # func (string) matches(string) bool mutation StringMatches { evaluate(expressions: { result: "'abc'.matches('^a.*c$')" }) # Example: true } ``` --- # Functions Standard functions included in the Twisp CEL runtime. Source: https://www.twisp.com/docs/reference/cel/functions CEL provides function invocation within expressions to perform operations and computations. Twisp includes a number of predefined extensions to the CEL runtime environment for computation within ledger transactions. ## Packages The included package functions are: 1. **Strongly-typed**: The function signatures of a package require that the arguments provided have the expected types. 1. **Side-effect-free**: Package functions only calculate an output based on the inputs given. The Twisp CEL runtime has been extended with the following packages and their respective function signatures. ### cal #### `func cal.Quarter(ts &{types Timestamp}, fiscalStartMonth int) int` Quarter returns the quarter based on the fiscalStartMonth: fiscalStartMonth: 1 (time.January) standard fiscalStartMonth: 10 (time.October) US Government #### `func cal.WeekOfYear(ts &{types Timestamp}, dow int, doy int) YearWeek` WeekOfYear returns the week of year given: - dow day of week that starts the week (Sunday:0 - Saturday: 6) - doy first January day that must appear in week 1. Common Settings: USA: dow 0 doy 1 (First January 1st in year) ISO: dow 1 doy 4 (First Thursday of year equivalent to first Jan 4 in year) #### `func cal.ISOWeekOfYear(ts &{types Timestamp}) YearWeek` ISOWeekOfYear returns the ISO year and week of the timestamp. See https://en.wikipedia.org/wiki/ISO_8601#Week_dates ### decimal #### `func decimal.Abs(x decimal) decimal, error` Abs calculates |x| (the absolute value of x). #### `func decimal.Add(x decimal, y decimal) decimal, error` Add calculates the sum of x+y. #### `func decimal.Cbrt(x decimal) decimal, error` Cbrt calculates the cube root of x. #### `func decimal.Ceil(x decimal) decimal, error` Ceil calculates smallest integer >= x. #### `func decimal.Cmp(x decimal, y decimal) decimal, error` Cmp compares x and y and calculates: ``` -1 if x < y 0 if x == y +1 if x > y ``` This comparison respects the normal rules of special values (like NaN), and does not compare them. #### `func decimal.Exp(x decimal, precision uint32) decimal, error` Exp calculates e**x. #### `func decimal.Ln(x decimal) decimal, error` Ln calculates the natural log of x. #### `func decimal.Log10(x decimal) decimal, error` Log10 calculates the base 10 log of x. #### `func decimal.Mul(x decimal, y decimal) decimal, error` Mul calculates the product x*y. #### `func decimal.Neg(x decimal) decimal, error` Neg calculates -x. #### `func decimal.Pow(x decimal, y decimal) decimal, error` Pow calculates x**y. #### `func decimal.Quantize(x decimal, exp int, precision uint32) decimal, error` Quantize calculates and rounds x as necessary so it is represented with exponent exp. #### `func decimal.Quo(x decimal, y decimal, precision uint32) decimal, error` Quo calculates the quotient x/y for y != 0. #### `func decimal.QuoInteger(x decimal, y decimal, precision uint32) decimal, error` QuoInteger calculates the integer part of the quotient x/y #### `func decimal.Reduce(x decimal) decimal, error` Reduce calculates x with all trailing zeros removed. #### `func decimal.Rem(x decimal, y decimal, precision uint32) decimal, error` Rem calculates the remainder part of the quotient x/y. #### `func decimal.Sqrt(x decimal) decimal, error` Sqrt calculates the square root of x. Sqrt uses the Babylonian method for computing the square root, which uses O(log p) steps for p digits of precision. #### `func decimal.Sub(x decimal, y decimal) decimal, error` Sub calculates the difference x-y. #### `func decimal.Round(x decimal, mode string, digits int) decimal, error` Round rounds x to the number of digits with the specified rounding mode. Supported rounding modes: ``` down ``` Rounds toward 0; truncate. ``` half_up ``` Rounds up if the digits are >= 0.5. ``` half_even ``` Rounds up if the digits are > 0.5. If the digits are equal to 0.5, it rounds up if the previous digit is odd, always producing an even digit. ``` ceiling ``` Rounds towards +Inf: rounds up if digits are > 0 and the number is positive. ``` floor ``` Rounds towards -Inf: rounds up if digits are > 0 and the number is negative. ``` half_down ``` Rounds up if the digits are > 0.5. ``` up ``` Rounds away from 0. ``` 05up ``` Rounds zero or five away from 0; same as round-up, except that rounding up only occurs if the digit to be rounded up is 0 or 5. #### `func decimal.Min(a decimal, b decimal) decimal` Min returns the min of a and b. If equal, returns b. #### `func decimal.Max(a decimal, b decimal) decimal` Max returns the max of a and b. If equal, returns b. ### finance #### `func finance.DaysDifference(date1 int64, date2 int64, basis int) int` DaysDifference returns the difference of days between two dates based on a daycount basis. Date1 and date2 are UNIX timestamps (seconds). "basis" must be one of: 0 = US(NASD) 30/360, 1 = Actual/actual, 2 = Actual/360, 3 = Actual/365, 4 = European 30/360. #### `func finance.DaysPerYear(year int, basis int) int` DaysPerYear returns the number of days in the year based on a daycount basis. "basis" must be one of: 0 = US(NASD) 30/360, 1 = Actual/actual, 2 = Actual/360, 3 = Actual/365, 4 = European 30/360. #### `func finance.DepreciationFixedDeclining(cost float64, salvage float64, life int, period int, month int) float64, error` DepreciationFixedDeclining returns the depreciation of an asset using the fixed-declining balance method. Excel equivalent: DB. "basis" must be one of: 0 = US(NASD) 30/360, 1 = Actual/actual, 2 = Actual/360, 3 = Actual/365, 4 = European 30/360. #### `func finance.DepreciationSYD(cost float64, salvage float64, life int, per int) float64` DepreciationSYD returns the depreciation for an asset in a given period using the sum-of-years' digits method. Excel equivalent: SYD. #### `func finance.DepreciationStraightLine(cost float64, salvage float64, life int) float64, error` DepreciationStraightLine returns the straight-line depreciation of an asset for each period. Excel equivalent: SLN. #### `func finance.DiscountRate(settlement int64, maturity int64, price float64, redemption float64, basis int) float64` DiscountRate returns the discount rate for a bond "settlement" is the unix timestamp (seconds) for the settlement date. "maturity" is the unix timestamp (seconds) for the maturity date. "price" is the bond's price per $100 face value. "redemption" is the bond's redemption value per $100 face value. Excel equivalent: DISC. "basis" must be one of: 0 = US(NASD) 30/360, 1 = Actual/actual, 2 = Actual/360, 3 = Actual/365, 4 = European 30/360. #### `func finance.EffectiveRate(nominal float64, numPeriods int) float64, error` EffectiveRate returns the effective interest rate given the nominal rate and the number of compounding payments per year. Excel equivalent: EFFECT. #### `func finance.FutureValue(rate float64, numPeriods int, pmt float64, pv float64, paymentType int)fv float64, err error` FutureValue returns the Future Value of a cash flow with constant payments and interest rate (annuities). Excel equivalent: FV. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.InterestPayment(rate float64, period int, numPeriods int, pv float64, fv float64, paymentType int) float64, error` InterestPayment returns the interest payment for a given period for a cash flow with constant periodic payments (annuities). Excel equivalent: IMPT. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.NominalRate(effectiveRate float64, numPeriods int) float64, error` NominalRate returns the nominal interest rate given the effective rate and the number of compounding payments per year. Excel equivalent: NOMINAL. #### `func finance.Payment(rate float64, numPeriods int, pv float64, fv float64, paymentType int)pmt float64, err error` Payment returns the constant payment (annuity) for a cash flow with a constant interest rate. Excel equivalent: PMT. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.Periods(rate float64, pmt float64, pv float64, fv float64, paymentType int)numPeriods float64, err error` Periods returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate. Excel equivalent: NPER. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.PresentValue(rate float64, numPeriods int, pmt float64, fv float64, paymentType int)pv float64, err error` PresentValue returns the Present Value of a cash flow with constant payments and interest rate (annuities). Excel equivalent: PV. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.PriceDiscount(settlement int64, maturity int64, discount float64, redemption float64, basis int) float64` PriceDiscount returns the price per $100 face value of a discounted bond. "settlement" is the unix timestamp (seconds) for the settlement date. "maturity" is the unix timestamp (seconds) for the maturity date. "discount" is the bond's discount rate. "redemption" is the bond's redemption value per $100 face value. Excel equivalent: PRICEDISC. "basis" must be one of: 0 = US(NASD) 30/360, 1 = Actual/actual, 2 = Actual/360, 3 = Actual/365, 4 = European 30/360. #### `func finance.PrincipalPayment(rate float64, period int, numPeriods int, pv float64, fv float64, paymentType int) float64, error` PrincipalPayment returns the principal payment for a given period for a cash flow with constant periodic payments (annuities). Excel equivalent: PPMT. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.Rate(numPeriods int, pmt float64, pv float64, fv float64, paymentType int, guess float64) float64, error` Rate returns the periodic interest rate for a cash flow with constant periodic payments (annuities). Guess is a guess for the rate, used as a starting point for the iterative algorithm. Excel equivalent: RATE. "paymentType" must be one of: 0 = PayEnd, 1 = PayBegin. #### `func finance.TBillEquivalentYield(settlement int64, maturity int64, discount float64) float64, error` TBillEquivalentYield returns the bond-equivalent yield for a Treasury bill. "settlement" is the unix timestamp (seconds) for the settlement date. "maturity" is the unix timestamp (seconds) for the maturity date. "discount" is the T-Bill discount rate. Excel equivalent: TBILLEQ. #### `func finance.TBillPrice(settlement int64, maturity int64, discount float64) float64, error` TBillPrice returns the price per $100 face value for a Treasury bill. "settlement" is the unix timestamp (seconds) for the settlement date. "maturity" is the unix timestamp (seconds) for the maturity date. "discount" is the T-Bill discount rate. Excel equivalent: TBILLPRICE. #### `func finance.TBillYield(settlement int64, maturity int64, price float64) float64, error` TBillYield returns the yield for a treasury bill. "settlement" is the unix timestamp (seconds) for the settlement date. "maturity" is the unix timestamp (seconds) for the maturity date. "price" is the TBill price per $100 face value. Excel equivalent: TBILLYIELD. ### hex #### `func hex.EncodeToString(src []byte) string` EncodeToString returns the hexadecimal encoding of src. #### `func hex.DecodeString(s string) []byte, error` DecodeString returns the bytes represented by the hexadecimal string s. DecodeString expects that src contains only hexadecimal characters and that src has even length. If the input is malformed, DecodeString returns the bytes decoded before the error. ### html #### `func html.EscapeString(s string) string` EscapeString escapes special characters like "<" to become "<". It escapes only five such characters: <, >, &, ' and ". `UnescapeString`(EscapeString(s)) == s always holds, but the converse isn't always true. #### `func html.UnescapeString(s string) string` UnescapeString unescapes entities like "<" to become "<". It unescapes a larger range of entities than `EscapeString` escapes. For example, "á" unescapes to "á", as does "á" and "á". UnescapeString(`EscapeString`(s)) == s always holds, but the converse isn't always true. ### json #### `func json.Marshal(v any) []byte, error` Marshal returns the JSON encoding of v. Marshal traverses the value v recursively. If an encountered value implements `Marshaler` and is not a nil pointer, Marshal calls [Marshaler.MarshalJSON] to produce JSON. If no [Marshaler.MarshalJSON] method is present but the value implements [encoding.TextMarshaler] instead, Marshal calls [encoding.TextMarshaler.MarshalText] and encodes the result as a JSON string. The nil pointer exception is not strictly necessary but mimics a similar, necessary exception in the behavior of [Unmarshaler.UnmarshalJSON]. Otherwise, Marshal uses the following type-dependent default encodings: Boolean values encode as JSON booleans. Floating point, integer, and `Number` values encode as JSON numbers. NaN and +/-Inf values will return an `UnsupportedValueError`. String values encode as JSON strings coerced to valid UTF-8, replacing invalid bytes with the Unicode replacement rune. So that the JSON will be safe to embed inside HTML tags, the string is encoded using `HTMLEscape`, which replaces "<", ">", "&", U+2028, and U+2029 are escaped to "\u003c","\u003e", "\u0026", "\u2028", and "\u2029". This replacement can be disabled when using an `Encoder`, by calling `Encoder.SetEscapeHTML(false)`. Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value. Struct values encode as JSON objects. Each exported struct field becomes a member of the object, using the field name as the object key, unless the field is omitted for one of the reasons given below. The encoding of each struct field can be customized by the format string stored under the "json" key in the struct field's tag. The format string gives the name of the field, possibly followed by a comma-separated list of options. The name may be empty in order to specify options without overriding the default field name. The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any array, slice, map, or string of length zero. As a special case, if the field tag is "-", the field is always omitted. Note that a field with name "-" can still be generated using the tag "-,". Examples of struct field tags and their meanings: ``` // Field appears in JSON as key "myName". Field int `json:"myName"` // Field appears in JSON as key "myName" and // the field is omitted from the object if its value is empty, // as defined above. Field int `json:"myName,omitempty"` // Field appears in JSON as key "Field" (the default), but // the field is skipped if empty. // Note the leading comma. Field int `json:",omitempty"` // Field is ignored by this package. Field int `json:"-"` // Field appears in JSON as key "-". Field int `json:"-,"` ``` The "omitzero" option specifies that the field should be omitted from the encoding if the field has a zero value, according to rules: 1) If the field type has an "IsZero() bool" method, that will be used to determine whether the value is zero. 2) Otherwise, the value is zero if it is the zero value for its type. If both "omitempty" and "omitzero" are specified, the field will be omitted if the value is either empty or zero (or both). The "string" option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types. This extra level of encoding is sometimes used when communicating with JavaScript programs: ``` Int64String int64 `json:",string"` ``` The key name will be used if it's a non-empty string consisting of only Unicode letters, digits, and ASCII punctuation except quotation marks, backslash, and comma. Embedded struct fields are usually marshaled as if their inner exported fields were fields in the outer struct, subject to the usual Go visibility rules amended as described in the next paragraph. An anonymous struct field with a name given in its JSON tag is treated as having that name, rather than being anonymous. An anonymous struct field of interface type is treated the same as having that type as its name, rather than being anonymous. The Go visibility rules for struct fields are amended for JSON when deciding which field to marshal or unmarshal. If there are multiple fields at the same level, and that level is the least nested (and would therefore be the nesting level selected by the usual Go rules), the following extra rules apply: 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, even if there are multiple untagged fields that would otherwise conflict. 2) If there is exactly one field (tagged or not according to the first rule), that is selected. 3) Otherwise there are multiple fields, and all are ignored; no error occurs. Handling of anonymous struct fields is new in Go 1.1. Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of an anonymous struct field in both current and earlier versions, give the field a JSON tag of "-". Map values encode as JSON objects. The map's key type must either be a string, an integer type, or implement [encoding.TextMarshaler]. The map keys are sorted and used as JSON object keys by applying the following rules, subject to the UTF-8 coercion described for string values above: - keys of any string type are used directly - keys that implement [encoding.TextMarshaler] are marshaled - integer keys are converted to strings Pointer values encode as the value pointed to. A nil pointer encodes as the null JSON value. Interface values encode as the value contained in the interface. A nil interface value encodes as the null JSON value. Channel, complex, and function values cannot be encoded in JSON. Attempting to encode such a value causes Marshal to return an `UnsupportedTypeError`. JSON cannot represent cyclic data structures and Marshal does not handle them. Passing cyclic structures to Marshal will result in an error. #### `func json.Marshal(v any) []byte, error` Marshal returns the JSON encoding of v. Marshal traverses the value v recursively. If an encountered value implements `Marshaler` and is not a nil pointer, Marshal calls [Marshaler.MarshalJSON] to produce JSON. If no [Marshaler.MarshalJSON] method is present but the value implements [encoding.TextMarshaler] instead, Marshal calls [encoding.TextMarshaler.MarshalText] and encodes the result as a JSON string. The nil pointer exception is not strictly necessary but mimics a similar, necessary exception in the behavior of [Unmarshaler.UnmarshalJSON]. Otherwise, Marshal uses the following type-dependent default encodings: Boolean values encode as JSON booleans. Floating point, integer, and `Number` values encode as JSON numbers. NaN and +/-Inf values will return an `UnsupportedValueError`. String values encode as JSON strings coerced to valid UTF-8, replacing invalid bytes with the Unicode replacement rune. So that the JSON will be safe to embed inside HTML tags, the string is encoded using `HTMLEscape`, which replaces "<", ">", "&", U+2028, and U+2029 are escaped to "\u003c","\u003e", "\u0026", "\u2028", and "\u2029". This replacement can be disabled when using an `Encoder`, by calling `Encoder.SetEscapeHTML(false)`. Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value. Struct values encode as JSON objects. Each exported struct field becomes a member of the object, using the field name as the object key, unless the field is omitted for one of the reasons given below. The encoding of each struct field can be customized by the format string stored under the "json" key in the struct field's tag. The format string gives the name of the field, possibly followed by a comma-separated list of options. The name may be empty in order to specify options without overriding the default field name. The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any array, slice, map, or string of length zero. As a special case, if the field tag is "-", the field is always omitted. JSON names containing commas or quotes, or names identical to "" or "-", can be specified using a single-quoted string literal, where the syntax is identical to the Go grammar for a double-quoted string literal, but instead uses single quotes as the delimiters. Examples of struct field tags and their meanings: ``` // Field appears in JSON as key "myName". Field int `json:"myName"` // Field appears in JSON as key "myName" and // the field is omitted from the object if its value is empty, // as defined above. Field int `json:"myName,omitempty"` // Field appears in JSON as key "Field" (the default), but // the field is skipped if empty. // Note the leading comma. Field int `json:",omitempty"` // Field is ignored by this package. Field int `json:"-"` // Field appears in JSON as key "-". Field int `json:"'-'"` ``` The "omitzero" option specifies that the field should be omitted from the encoding if the field has a zero value, according to rules: 1) If the field type has an "IsZero() bool" method, that will be used to determine whether the value is zero. 2) Otherwise, the value is zero if it is the zero value for its type. If both "omitempty" and "omitzero" are specified, the field will be omitted if the value is either empty or zero (or both). The "string" option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types. This extra level of encoding is sometimes used when communicating with JavaScript programs: ``` Int64String int64 `json:",string"` ``` The key name will be used if it's a non-empty string consisting of only Unicode letters, digits, and ASCII punctuation except quotation marks, backslash, and comma. Embedded struct fields are usually marshaled as if their inner exported fields were fields in the outer struct, subject to the usual Go visibility rules amended as described in the next paragraph. An anonymous struct field with a name given in its JSON tag is treated as having that name, rather than being anonymous. An anonymous struct field of interface type is treated the same as having that type as its name, rather than being anonymous. The Go visibility rules for struct fields are amended for JSON when deciding which field to marshal or unmarshal. If there are multiple fields at the same level, and that level is the least nested (and would therefore be the nesting level selected by the usual Go rules), the following extra rules apply: 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, even if there are multiple untagged fields that would otherwise conflict. 2) If there is exactly one field (tagged or not according to the first rule), that is selected. 3) Otherwise there are multiple fields, and all are ignored; no error occurs. Handling of anonymous struct fields is new in Go 1.1. Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of an anonymous struct field in both current and earlier versions, give the field a JSON tag of "-". Map values encode as JSON objects. The map's key type must either be a string, an integer type, or implement [encoding.TextMarshaler]. The map keys are sorted and used as JSON object keys by applying the following rules, subject to the UTF-8 coercion described for string values above: - keys of any string type are used directly - keys that implement [encoding.TextMarshaler] are marshaled - integer keys are converted to strings Pointer values encode as the value pointed to. A nil pointer encodes as the null JSON value. Interface values encode as the value contained in the interface. A nil interface value encodes as the null JSON value. Channel, complex, and function values cannot be encoded in JSON. Attempting to encode such a value causes Marshal to return an `UnsupportedTypeError`. JSON cannot represent cyclic data structures and Marshal does not handle them. Passing cyclic structures to Marshal will result in an error. ### math #### `func math.Abs(x float64) float64` Abs returns the absolute value of x. Special cases are: ``` Abs(±Inf) = +Inf Abs(NaN) = NaN ``` #### `func math.Cbrt(x float64) float64` Cbrt returns the cube root of x. Special cases are: ``` Cbrt(±0) = ±0 Cbrt(±Inf) = ±Inf Cbrt(NaN) = NaN ``` #### `func math.Copysign(f float64, sign float64) float64` Copysign returns a value with the magnitude of f and the sign of sign. #### `func math.Dim(x float64, y float64) float64` Dim returns the maximum of x-y or 0. Special cases are: ``` Dim(+Inf, +Inf) = NaN Dim(-Inf, -Inf) = NaN Dim(x, NaN) = Dim(NaN, x) = NaN ``` #### `func math.Max(x float64, y float64) float64` Max returns the larger of x or y. Special cases are: ``` Max(x, +Inf) = Max(+Inf, x) = +Inf Max(x, NaN) = Max(NaN, x) = NaN Max(+0, ±0) = Max(±0, +0) = +0 Max(-0, -0) = -0 ``` Note that this differs from the built-in function max when called with NaN and +Inf. #### `func math.Min(x float64, y float64) float64` Min returns the smaller of x or y. Special cases are: ``` Min(x, -Inf) = Min(-Inf, x) = -Inf Min(x, NaN) = Min(NaN, x) = NaN Min(-0, ±0) = Min(±0, -0) = -0 ``` Note that this differs from the built-in function min when called with NaN and -Inf. #### `func math.Exp(x float64) float64` Exp returns e**x, the base-e exponential of x. Special cases are: ``` Exp(+Inf) = +Inf Exp(NaN) = NaN ``` Very large values overflow to 0 or +Inf. Very small values underflow to 1. #### `func math.Exp2(x float64) float64` Exp2 returns 2**x, the base-2 exponential of x. Special cases are the same as `Exp`. #### `func math.Expm1(x float64) float64` Expm1 returns e**x - 1, the base-e exponential of x minus 1. It is more accurate than `Exp`(x) - 1 when x is near zero. Special cases are: ``` Expm1(+Inf) = +Inf Expm1(-Inf) = -1 Expm1(NaN) = NaN ``` Very large values overflow to -1 or +Inf. #### `func math.Floor(x float64) float64` Floor returns the greatest integer value less than or equal to x. Special cases are: ``` Floor(±0) = ±0 Floor(±Inf) = ±Inf Floor(NaN) = NaN ``` #### `func math.Ceil(x float64) float64` Ceil returns the least integer value greater than or equal to x. Special cases are: ``` Ceil(±0) = ±0 Ceil(±Inf) = ±Inf Ceil(NaN) = NaN ``` #### `func math.Trunc(x float64) float64` Trunc returns the integer value of x. Special cases are: ``` Trunc(±0) = ±0 Trunc(±Inf) = ±Inf Trunc(NaN) = NaN ``` #### `func math.Round(x float64) float64` Round returns the nearest integer, rounding half away from zero. Special cases are: ``` Round(±0) = ±0 Round(±Inf) = ±Inf Round(NaN) = NaN ``` #### `func math.RoundToEven(x float64) float64` RoundToEven returns the nearest integer, rounding ties to even. Special cases are: ``` RoundToEven(±0) = ±0 RoundToEven(±Inf) = ±Inf RoundToEven(NaN) = NaN ``` #### `func math.FMA(x float64, y float64, z float64) float64` FMA returns x * y + z, computed with only one rounding. (That is, FMA returns the fused multiply-add of x, y, and z.) #### `func math.Hypot(p float64, q float64) float64` Hypot returns `Sqrt`(p*p + q*q), taking care to avoid unnecessary overflow and underflow. Special cases are: ``` Hypot(±Inf, q) = +Inf Hypot(p, ±Inf) = +Inf Hypot(NaN, q) = NaN Hypot(p, NaN) = NaN ``` #### `func math.Log(x float64) float64` Log returns the natural logarithm of x. Special cases are: ``` Log(+Inf) = +Inf Log(0) = -Inf Log(x < 0) = NaN Log(NaN) = NaN ``` #### `func math.Log10(x float64) float64` Log10 returns the decimal logarithm of x. The special cases are the same as for `Log`. #### `func math.Log2(x float64) float64` Log2 returns the binary logarithm of x. The special cases are the same as for `Log`. #### `func math.Log1p(x float64) float64` Log1p returns the natural logarithm of 1 plus its argument x. It is more accurate than `Log`(1 + x) when x is near zero. Special cases are: ``` Log1p(+Inf) = +Inf Log1p(±0) = ±0 Log1p(-1) = -Inf Log1p(x < -1) = NaN Log1p(NaN) = NaN ``` #### `func math.Mod(x float64, y float64) float64` Mod returns the floating-point remainder of x/y. The magnitude of the result is less than y and its sign agrees with that of x. Special cases are: ``` Mod(±Inf, y) = NaN Mod(NaN, y) = NaN Mod(x, 0) = NaN Mod(x, ±Inf) = x Mod(x, NaN) = NaN ``` #### `func math.Pow(x float64, y float64) float64` Pow returns x**y, the base-x exponential of y. Special cases are (in order): ``` Pow(x, ±0) = 1 for any x Pow(1, y) = 1 for any y Pow(x, 1) = x for any x Pow(NaN, y) = NaN Pow(x, NaN) = NaN Pow(±0, y) = ±Inf for y an odd integer < 0 Pow(±0, -Inf) = +Inf Pow(±0, +Inf) = +0 Pow(±0, y) = +Inf for finite y < 0 and not an odd integer Pow(±0, y) = ±0 for y an odd integer > 0 Pow(±0, y) = +0 for finite y > 0 and not an odd integer Pow(-1, ±Inf) = 1 Pow(x, +Inf) = +Inf for |x| > 1 Pow(x, -Inf) = +0 for |x| > 1 Pow(x, +Inf) = +0 for |x| < 1 Pow(x, -Inf) = +Inf for |x| < 1 Pow(+Inf, y) = +Inf for y > 0 Pow(+Inf, y) = +0 for y < 0 Pow(-Inf, y) = Pow(-0, -y) Pow(x, y) = NaN for finite x < 0 and finite non-integer y ``` #### `func math.Pow10(n int) float64` Pow10 returns 10**n, the base-10 exponential of n. Special cases are: ``` Pow10(n) = 0 for n < -323 Pow10(n) = +Inf for n > 308 ``` #### `func math.Remainder(x float64, y float64) float64` Remainder returns the IEEE 754 floating-point remainder of x/y. Special cases are: ``` Remainder(±Inf, y) = NaN Remainder(NaN, y) = NaN Remainder(x, 0) = NaN Remainder(x, ±Inf) = x Remainder(x, NaN) = NaN ``` #### `func math.Signbit(x float64) bool` Signbit reports whether x is negative or negative zero. #### `func math.Sqrt(x float64) float64` Sqrt returns the square root of x. Special cases are: ``` Sqrt(+Inf) = +Inf Sqrt(±0) = ±0 Sqrt(x < 0) = NaN Sqrt(NaN) = NaN ``` ### md5 #### `func md5.Sum(data []byte) []byte` Sum returns the MD5 checksum of the data. ### money #### `func money.Add(a money, b money) money, error` Add two Money types and return the result #### `func money.Sub(a money, b money) money, error` Compute the difference between two Money types #### `func money.Mul(a money, b string) money, error` Multiply a Money type by a string-represented number #### `func money.Div(a money, b string) money, error` Divide a Money type by a string-represented number ### path #### `func path.Clean(path string) string` Clean returns the shortest path name equivalent to path by purely lexical processing. It applies the following rules iteratively until no further processing can be done: 1. Replace multiple slashes with a single slash. 2. Eliminate each . path name element (the current directory). 3. Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it. 4. Eliminate .. elements that begin a rooted path: that is, replace "/.." by "/" at the beginning of a path. The returned path ends in a slash only if it is the root "/". If the result of this process is an empty string, Clean returns the string ".". See also Rob Pike, “Lexical File Names in Plan 9 or Getting Dot-Dot Right,” https://9p.io/sys/doc/lexnames.html #### `func path.Ext(path string) string` Ext returns the file name extension used by path. The extension is the suffix beginning at the final dot in the final slash-separated element of path; it is empty if there is no dot. #### `func path.Base(path string) string` Base returns the last element of path. Trailing slashes are removed before extracting the last element. If the path is empty, Base returns ".". If the path consists entirely of slashes, Base returns "/". #### `func path.IsAbs(path string) bool` IsAbs reports whether the path is absolute. #### `func path.Dir(path string) string` Dir returns all but the last element of path, typically the path's directory. After dropping the final element using `Split`, the path is Cleaned and trailing slashes are removed. If the path is empty, Dir returns ".". If the path consists entirely of slashes followed by non-slash bytes, Dir returns a single slash. In any other case, the returned path does not end in a slash. ### rand #### `func rand.Int63() int64` Int63 returns a non-negative pseudo-random 63-bit integer as an int64 from the default `Source`. #### `func rand.Uint32() uint32` Uint32 returns a pseudo-random 32-bit value as a uint32 from the default `Source`. #### `func rand.Uint64() uint64` Uint64 returns a pseudo-random 64-bit value as a uint64 from the default `Source`. #### `func rand.Int31() int32` Int31 returns a non-negative pseudo-random 31-bit integer as an int32 from the default `Source`. #### `func rand.Int() int` Int returns a non-negative pseudo-random int from the default `Source`. #### `func rand.Int63n(n int64) int64` Int63n returns, as an int64, a non-negative pseudo-random number in the half-open interval [0,n) from the default `Source`. It panics if n <= 0. #### `func rand.Int31n(n int32) int32` Int31n returns, as an int32, a non-negative pseudo-random number in the half-open interval [0,n) from the default `Source`. It panics if n <= 0. #### `func rand.Intn(n int) int` Intn returns, as an int, a non-negative pseudo-random number in the half-open interval [0,n) from the default `Source`. It panics if n <= 0. #### `func rand.Float64() float64` Float64 returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0) from the default `Source`. #### `func rand.Float32() float32` Float32 returns, as a float32, a pseudo-random number in the half-open interval [0.0,1.0) from the default `Source`. #### `func rand.NormFloat64() float64` NormFloat64 returns a normally distributed float64 in the range [-[math.MaxFloat64], +[math.MaxFloat64]] with standard normal distribution (mean = 0, stddev = 1) from the default `Source`. To produce a different normal distribution, callers can adjust the output using: ``` sample = NormFloat64() * desiredStdDev + desiredMean ``` #### `func rand.ExpFloat64() float64` ExpFloat64 returns an exponentially distributed float64 in the range (0, +[math.MaxFloat64]] with an exponential distribution whose rate parameter (lambda) is 1 and whose mean is 1/lambda (1) from the default `Source`. To produce a distribution with a different rate parameter, callers can adjust the output using: ``` sample = ExpFloat64() / desiredRateParameter ``` ### sha1 #### `func sha1.Sum(data []byte) []byte` Sum returns the SHA-1 checksum of the data. ### sha256 #### `func sha256.Sum256(data []byte) []byte` Sum256 returns the SHA256 checksum of the data. ### sha512 #### `func sha512.Sum512(data []byte) []byte` Sum512 returns the SHA512 checksum of the data. ### strings #### `func strings.Compare(a string, b string) int` Compare returns an integer comparing two strings lexicographically. The result will be 0 if a == b, -1 if a < b, and +1 if a > b. Use Compare when you need to perform a three-way comparison (with [slices.SortFunc], for example). It is usually clearer and always faster to use the built-in string comparison operators ==, <, >, and so on. #### `func strings.Count(s string, substr string) int` Count counts the number of non-overlapping instances of substr in s. If substr is an empty string, Count returns 1 + the number of Unicode code points in s. #### `func strings.Contains(s string, substr string) bool` Contains reports whether substr is within s. #### `func strings.ContainsAny(s string, chars string) bool` ContainsAny reports whether any Unicode code points in chars are within s. #### `func strings.LastIndex(s string, substr string) int` LastIndex returns the index of the last instance of substr in s, or -1 if substr is not present in s. #### `func strings.IndexAny(s string, chars string) int` IndexAny returns the index of the first instance of any Unicode code point from chars in s, or -1 if no Unicode code point from chars is present in s. #### `func strings.LastIndexAny(s string, chars string) int` LastIndexAny returns the index of the last instance of any Unicode code point from chars in s, or -1 if no Unicode code point from chars is present in s. #### `func strings.HasPrefix(s string, prefix string) bool` HasPrefix reports whether the string s begins with prefix. #### `func strings.HasSuffix(s string, suffix string) bool` HasSuffix reports whether the string s ends with suffix. #### `func strings.Repeat(s string, count int) string` Repeat returns a new string consisting of count copies of the string s. It panics if count is negative or if the result of (len(s) * count) overflows. #### `func strings.ToUpper(s string) string` ToUpper returns s with all Unicode letters mapped to their upper case. #### `func strings.ToLower(s string) string` ToLower returns s with all Unicode letters mapped to their lower case. #### `func strings.ToTitle(s string) string` ToTitle returns a copy of the string s with all Unicode letters mapped to their Unicode title case. #### `func strings.ToValidUTF8(s string, replacement string) string` ToValidUTF8 returns a copy of the string s with each run of invalid UTF-8 byte sequences replaced by the replacement string, which may be empty. #### `func strings.Title(s string) string` Title returns a copy of the string s with all Unicode letters that begin words mapped to their Unicode title case. Deprecated: The rule Title uses for word boundaries does not handle Unicode punctuation properly. Use golang.org/x/text/cases instead. #### `func strings.Trim(s string, cutset string) string` Trim returns a slice of the string s with all leading and trailing Unicode code points contained in cutset removed. #### `func strings.TrimLeft(s string, cutset string) string` TrimLeft returns a slice of the string s with all leading Unicode code points contained in cutset removed. To remove a prefix, use `TrimPrefix` instead. #### `func strings.TrimRight(s string, cutset string) string` TrimRight returns a slice of the string s, with all trailing Unicode code points contained in cutset removed. To remove a suffix, use `TrimSuffix` instead. #### `func strings.TrimSpace(s string) string` TrimSpace returns a slice (substring) of the string s, with all leading and trailing white space removed, as defined by Unicode. #### `func strings.TrimPrefix(s string, prefix string) string` TrimPrefix returns s without the provided leading prefix string. If s doesn't start with prefix, s is returned unchanged. #### `func strings.TrimSuffix(s string, suffix string) string` TrimSuffix returns s without the provided trailing suffix string. If s doesn't end with suffix, s is returned unchanged. #### `func strings.Replace(s string, old string, new string, n int) string` Replace returns a copy of the string s with the first n non-overlapping instances of old replaced by new. If old is empty, it matches at the beginning of the string and after each UTF-8 sequence, yielding up to k+1 replacements for a k-rune string. If n < 0, there is no limit on the number of replacements. #### `func strings.ReplaceAll(s string, old string, new string) string` ReplaceAll returns a copy of the string s with all non-overlapping instances of old replaced by new. If old is empty, it matches at the beginning of the string and after each UTF-8 sequence, yielding up to k+1 replacements for a k-rune string. #### `func strings.EqualFold(s string, t string) bool` EqualFold reports whether s and t, interpreted as UTF-8 strings, are equal under simple Unicode case-folding, which is a more general form of case-insensitivity. #### `func strings.Index(s string, substr string) int` Index returns the index of the first instance of substr in s, or -1 if substr is not present in s. ### url #### `func url.QueryEscape(s string) string` QueryEscape escapes the string so it can be safely placed inside a `URL` query. #### `func url.PathEscape(s string) string` PathEscape escapes the string so it can be safely placed inside a `URL` path segment, replacing special characters (including /) with %XX sequences as needed. ### uuid #### `func uuid.New() uuid` New creates a new random UUID. #### `func uuid.NewMD5(space uuid, data []byte) uuid` NewMD5 returns a new MD5 (Version 3) UUID based on the supplied name space and data. #### `func uuid.NewSHA1(space uuid, data []byte) uuid` NewSHA1 returns a new SHA1 (Version 5) UUID based on the supplied name space and data. #### `func uuid.Zero() uuid` Zero returns the zero uuid('00000000-0000-0000-0000-000000000000') --- # CEL Reference Embedded computation with the common expression language runtime. Source: https://www.twisp.com/docs/reference/cel Twisp uses the [Common Expression Language](https://github.com/google/cel-spec) extensively for computation. Anywhere the Expression type is used in GraphQL, a CEL expression is accepted and will be evaluated at runtime. - [Variables Reference](/docs/reference/cel/variables): Context variables available in indexes, calculations, and tran codes. - [Functions and Examples](/docs/reference/cel/examples): Comprehensive examples of functions provided by the runtime. - [Functions](/docs/reference/cel/functions): Standard library functions provided by the runtime. --- # CEL Variables Reference Context variables available in CEL expressions across Twisp APIs. Source: https://www.twisp.com/docs/reference/cel/variables Twisp uses [Common Expression Language](https://cel.dev/) extensively for computation across the API. Different contexts expose different variables depending on their purpose. This reference documents what variables are available in each context. ## Overview | Context | Primary Variable | Description | |-------------------------------|------------------|---------------------------------| | [Indexes](#indexes) | `document` | The record being indexed | | [Calculations](#calculations) | `context.vars` | Transaction and account context | | [Tran Codes](#tran-codes) | `params` | Post-time parameters | --- ## Indexes Custom indexes use CEL expressions to define partition keys, sort keys, and filter constraints. In all index expressions, the primary variable is `document`, which represents the record being indexed. ### Available Variables | Variable | Type | Description | |------------|------------------|--------------------------| | `document` | Protobuf message | The entity being indexed | The type of `document` depends on the `on` field specified when creating the index: - `ACCOUNT` → [Account](#account-fields) - `ACCOUNT_SET` → [AccountSet](#account-set-fields) - `TRANSACTION` → [Transaction](#transaction-fields) - `TRANCODE` → [TranCode](#trancode-fields) - `BALANCE` → [Balance](#balance-fields) - `ENTRY` → [Entry](#entry-fields) ### Partition Expressions Partition keys determine how records are grouped in the index. ```graphql mutation CreatePartitionedIndex { schema { createIndex( input: { name: "entries_by_account" on: Entry partition: [ { alias: "accountId", value: "document.account_id" } { alias: "journalId", value: "document.journal_id" } ] } ) { name } } } ``` Partition expressions can return arrays to create multiple index entries. For example this create multiple partitions for the account id and each of it's parent account sets (at time of posting): ```graphql partition: [ { alias: "accountId", value: "document.parent_account_ids + [document.account_id]" } ] ``` ### Sort Expressions Sort keys define the ordering within each partition. In this example sorting either by th ```graphql mutation CreateSortedIndex { schema { createIndex( input: { name: "entries_by_effective" on: Entry partition: [{ alias: "accountId", value: "document.account_id" }] sort: [ { alias: "effective" value: "'effective' in document.metadata && document.metadata.effective != null ? document.metadata.effective : document.created" type: STRING sort: DESC } ] } ) { name } } } ``` ### Filter Constraints Constraints are boolean expressions that must all evaluate to `true` for a record to be included in the index. ```graphql mutation CreateFilteredIndex { schema { createIndex( input: { name: "active_entries" on: Entry partition: [{ alias: "accountId", value: "document.account_id" }] constraints: { isNotVoidEntry: "!document.is_void_entry" isNotVoidedEntry: "!document.is_voided_entry" isSettled: "document.layer == SETTLED" } } ) { name } } } ``` --- ## Calculations Calculations define how balances are computed and grouped. CEL expressions are used in dimension definitions and conditions. > **Note:** > > **Expressions must resolve to a concrete type.** When you call `createCalculation`, each dimension expression is evaluated against an example record so its storage type can be inferred, and the `condition` expression must resolve to a boolean. Paths that reach into `dyn` JSON fields — like `context.vars.account.metadata.*` or `context.vars.entry.metadata.*` — won't resolve on their own. Two ways to fix this: > > - **Cast the result**: `string(context.vars.account.metadata.region)`, `int(context.vars.entry.metadata.score)`, `bool(context.vars.account.metadata.verified)`. > - **Provide a typed fallback** with optional access: `context.vars.account.?metadata.orValue({}).?region.orValue('')` resolves to a string even when `metadata.region` is missing. > > The same rule applies to `condition` — it must resolve to a `bool`. Use `bool(...)` or `.orValue(false)` if your expression reaches into untyped metadata. ### Available Variables | Variable | Type | Description | |----------|------|-------------| | `context.vars.transaction` | [Transaction](#transaction-fields) | The parent transaction of the entry | | `context.vars.account` | [Account](#account-fields) | The account the entry is posted to | | `document` | [Entry](#entry-fields) | The entry being evaluated (in conditions) | ### Dimension Expressions Dimensions define how balances are grouped. Common use cases include grouping by date, metadata values, or account properties. ```graphql mutation CreateEffectiveDateCalculation { createCalculation( input: { calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" code: "EFFECTIVE_DATE" description: "Track balances per effective date." dimensions: [ { alias: "effectiveDate" value: "context.vars.transaction.effective" } ] scope: LOCAL } ) { calculationId code } } ``` Multi-dimensional calculations: ```graphql mutation CreateMultiDimensionCalculation { createCalculation( input: { calculationId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" code: "BY_CURRENCY_AND_DATE" description: "Track balances by currency and effective date." dimensions: [ { alias: "currency" value: "string(context.vars.account.metadata.currency)" } { alias: "effectiveDate" value: "context.vars.transaction.effective" } ] scope: LOCAL } ) { calculationId } } ``` ### Condition Expressions Conditions filter which entries are included in the calculation. They must evaluate to a boolean. ```graphql mutation CreateConditionalCalculation { createCalculation( input: { calculationId: "b2c3d4e5-f6a7-8901-bcde-f23456789012" code: "POLICY_PAYMENTS" description: "Track only entries to accounts with policy payment metadata." dimensions: [ { alias: "effectiveDate", value: "context.vars.transaction.effective" } ] condition: "has(context.vars.account.metadata.policyPayment)" scope: LOCAL } ) { calculationId } } ``` Using entry fields in conditions: ```graphql condition: "document.layer == 0 && document.direction == 0" ``` --- ## Tran Codes Tran codes are templates that define how transactions and entries are created. CEL expressions are used extensively to dynamically compute values at post time. ### Available Variables | Variable | Type | Description | |----------|------|-------------| | `params` | Struct | Parameters passed when posting the transaction | | `vars` | Struct | Calculated variables defined in the tran code's `vars` field | ### Parameter Access Parameters are defined in the tran code and passed at post time. Access them with `params.`: ```graphql mutation CreateTranCode { createTranCode( input: { code: "TRANSFER" description: "Transfer funds between accounts" params: [ { name: "amount", type: TYPE_DECIMAL, description: "Transfer amount" } { name: "fromAccount", type: TYPE_UUID, description: "Source account" } { name: "toAccount", type: TYPE_UUID, description: "Destination account" } { name: "memo", type: TYPE_STRING, default: "'Transfer'", description: "Optional memo" } ] transaction: { description: "'Transfer ' + string(params.amount) + ' - ' + params.memo" } entries: [ { entryType: "'TRANSFER_DR'" accountId: "params.fromAccount" direction: "'DEBIT'" units: "params.amount" currency: "'USD'" } { entryType: "'TRANSFER_CR'" accountId: "params.toAccount" direction: "'CREDIT'" units: "params.amount" currency: "'USD'" } ] } ) { code } } ``` ### Vars (Scratch Pad) The `vars` field provides a scratch pad for intermediate calculations. Variables defined here can be referenced in transaction and entry expressions: ```graphql mutation CreateTranCodeWithVars { createTranCode( input: { code: "TRANSFER_WITH_FEE" description: "Transfer with calculated fee" params: [ { name: "amount", type: TYPE_DECIMAL } { name: "fromAccount", type: TYPE_UUID } { name: "toAccount", type: TYPE_UUID } { name: "feeAccount", type: TYPE_UUID } ] vars: { feeRate: "'0.025'" feeAmount: "decimal(params.amount) * decimal(vars.feeRate)" netAmount: "decimal(params.amount) - vars.feeAmount" } entries: [ { entryType: "'TRANSFER_DR'" accountId: "params.fromAccount" direction: "'DEBIT'" units: "params.amount" currency: "'USD'" } { entryType: "'TRANSFER_CR'" accountId: "params.toAccount" direction: "'CREDIT'" units: "vars.netAmount" currency: "'USD'" } { entryType: "'FEE_CR'" accountId: "params.feeAccount" direction: "'CREDIT'" units: "vars.feeAmount" currency: "'USD'" } ] } ) { code } } ``` ### Entry Conditions Entry conditions determine whether an entry should be created. Useful for optional entries: ```graphql entries: [ { entryType: "'FEE'" accountId: "params.feeAccount" direction: "'CREDIT'" units: "params.feeAmount" currency: "'USD'" condition: "params.feeAmount > decimal('0.00')" } ] ``` ### Transaction Fields All fields in the `transaction` block accept CEL expressions: | Field | Expected Type | Example | |-------|--------------|---------| | `journalId` | UUID | `"uuid('b28f5684-0834-4292-8016-d2f2fb0367a9')"` | | `correlationId` | String | `"params.correlationId"` | | `externalId` | String | `"params.externalId"` | | `effective` | Date | `"date('2024-01-15')"` or `"params.effectiveDate"` | | `description` | String | `"'Payment: ' + string(params.amount)"` | | `metadata` | JSON | `"{ 'source': 'api', 'amount': string(params.amount) }"` | ### Entry Fields All fields in each entry accept CEL expressions: | Field | Expected Type | Example | |-------|--------------|---------| | `entryType` | String | `"'ACH_CREDIT'"` | | `accountId` | UUID | `"params.accountId"` | | `layer` | Enumeration | `"SETTLED"` or `"PENDING"` or `"ENCUMBRANCE"` | | `direction` | Enumeration | `"DEBIT"` or `"CREDIT"` | | `units` | Decimal | `"params.amount"` or `"decimal('100.00')"` | | `currency` | String | `"'USD'"` or `"params.currency"` | | `description` | String | `"'Entry for ' + params.memo"` | | `metadata` | CEL Map/JSON | `"{ 'lineItem': params.lineItem }"` | | `condition` | Boolean | `"params.amount > decimal('0')"` | --- ## Entity Field Reference ### Account Fields | Field | Type | Description | |-------|------|-------------| | `account_id` | UUID | Unique identifier | | `status` | Enum | ACTIVE, LOCKED, or INACTIVE | | `name` | String | Account name | | `code` | String | Shorthand code | | `normal_balance_type` | Enum | DEBIT (0) or CREDIT (1) | | `description` | String | Account description | | `metadata` | Struct | Arbitrary JSON data | | `created` | Timestamp | Creation time | | `modified` | Timestamp | Last modification time | | `external_id` | String | External system identifier | ### Account Set Fields | Field | Type | Description | |-------|------|-------------| | `account_set_id` | UUID | Unique identifier | | `journal_id` | UUID | Associated journal | | `account_id` | UUID | Associated account ID | | `name` | String | Set name | | `description` | String | Set description | | `metadata` | Struct | Arbitrary JSON data | | `created` | Timestamp | Creation time | | `modified` | Timestamp | Last modification time | | `code` | String | Unique code | | `has_members` | Boolean | Whether set has members | ### Transaction Fields | Field | Type | Description | |-------|------|-------------| | `transaction_id` | UUID | Unique identifier | | `tran_code_id` | UUID | Associated tran code | | `journal_id` | UUID | Associated journal | | `correlation_id` | String | Groups related transactions | | `external_id` | String | External system identifier | | `effective` | Date | Accounting effective date | | `description` | String | Transaction description | | `metadata` | Struct | Arbitrary JSON data | | `created` | Timestamp | Creation time | | `modified` | Timestamp | Last modification time | ### Entry Fields | Field | Type | Description | |-------|------|-------------| | `entry_id` | UUID | Unique identifier | | `transaction_id` | UUID | Parent transaction | | `journal_id` | UUID | Associated journal | | `account_id` | UUID | Target account | | `entry_type` | String | Entry type code | | `layer` | Enum | SETTLED (0), PENDING (1), or ENCUMBRANCE (2) | | `direction` | Enum | DEBIT (0) or CREDIT (1) | | `description` | String | Entry description | | `amount` | Money | Entry amount with currency | | `metadata` | Struct | Arbitrary JSON data | | `created` | Timestamp | Creation time | | `modified` | Timestamp | Last modification time | | `parent_account_ids` | List[UUID] | Parent account set IDs | | `is_void_entry` | Boolean | True if this is a voiding entry | | `is_voided_entry` | Boolean | True if this entry was voided | ### TranCode Fields | Field | Type | Description | |-------|------|-------------| | `tran_code_id` | UUID | Unique identifier | | `code` | String | Unique code identifier | | `description` | String | Tran code description | | `status` | Enum | ACTIVE, LOCKED, or INACTIVE | | `params` | List | Parameter definitions | | `transaction` | Struct | Transaction template | | `entries` | List | Entry templates | | `metadata` | Struct | Arbitrary JSON data | | `created` | Timestamp | Creation time | | `modified` | Timestamp | Last modification time | ### Balance Fields | Field | Type | Description | |-------|------|-------------| | `journal_id` | UUID | Associated journal | | `account_id` | UUID | Associated account | | `transaction_id` | UUID | Last transaction | | `entry_id` | UUID | Last entry | | `currency` | String | Currency code | | `settled` | BalanceAmount | Settled layer balance | | `pending` | BalanceAmount | Pending layer balance | | `encumbrance` | BalanceAmount | Encumbrance layer balance | | `created` | Timestamp | Creation time | | `modified` | Timestamp | Last modification time | | `calculation_id` | UUID | Associated calculation | | `dimensions` | Struct | Dimension values | --- ## Type Constructors When working with CEL expressions, use these constructors to create typed values: | Constructor | Example | Description | |------------|---------|-------------| | `uuid(string)` | `uuid('a1b2c3d4-...')` | Parse UUID from string | | `decimal(string)` | `decimal('100.50')` | High-precision decimal | | `money(string, string)` | `money('100.00', 'USD')` | Money with currency | | `date(string)` | `date('2024-01-15')` | Parse date (YYYY-MM-DD) | | `timestamp(string)` | `timestamp('2024-01-15T10:30:00Z')` | Parse ISO 8601 timestamp | | `bool(string)` | `bool('true')` | Parse boolean | | `int(string)` | `int('42')` | Parse integer | | `string(any)` | `string(params.amount)` | Convert to string | For a complete list of functions, see the [CEL Functions Reference](/docs/reference/cel/examples). --- # Directives Directives provide a way to add metadata and modify the behavior of GraphQL operations, fields, and types. Source: https://www.twisp.com/docs/reference/graphql/directives ## @cel Denotes that the variable definition is resolved via a CEL Expression. The default value provided to the string is the CEL expression. And the cel value must resolve/convert to the graphql type indicated by the variable. @example(`$someVariable: String = "string('hello world')" @cel`) #### Locations ``VARIABLE_DEFINITION`` ## @deprecated Marks an element of a GraphQL schema as no longer supported. #### Arguments --- * ``reason`` - [`String`](/docs/reference/graphql/types/scalar#string) * Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). _Default:_ ``"No longer supported"`` #### Locations ``FIELD_DEFINITION``, ``ARGUMENT_DEFINITION``, ``INPUT_FIELD_DEFINITION``, ``ENUM_VALUE`` ## @dryRun Allows to dryRun a mutation without committing the data. #### Locations ``MUTATION`` ## @export #### Arguments --- * ``as`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``cel`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A literal CEL expression to be evaluated. #### Locations ``FIELD`` ## @include Directs the executor to include this field or fragment only when the `if` argument is true. #### Arguments --- * ``if`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Included when true. #### Locations ``FIELD``, ``FRAGMENT_SPREAD``, ``INLINE_FRAGMENT`` ## @retry Twisp will retry retriable errors for up to duration automatically. Uses a golang duration string. If the value is less than or equal to zero, no retries performed. @example(`@retry(for:"30s")`) #### Arguments --- * ``for`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. _Default:_ ``"30s"`` #### Locations ``MUTATION``, ``QUERY`` ## @skip Directs the executor to skip this field or fragment when the `if` argument is true. #### Arguments --- * ``if`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Skipped when true. #### Locations ``FIELD``, ``FRAGMENT_SPREAD``, ``INLINE_FRAGMENT`` ## @specifiedBy Exposes a URL that specifies the behavior of this scalar. #### Arguments --- * ``url`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The URL that specifies the behavior of this scalar. #### Locations ``SCALAR`` ## @tx Sets the transaction isolation level for the operation. @example(`@tx(isolation: SERIALIZABLE)`) #### Arguments --- * ``isolation`` - [`TxIsolation`](/docs/reference/graphql/types/enum#tx-isolation) * Transaction isolation level. Different isolation levels provide different guarantees about data visibility and consistency. _Default:_ ``SNAPSHOT`` #### Locations ``MUTATION``, ``QUERY`` --- # GraphQL API Reference Documentation for the Twisp GraphQL API. Source: https://www.twisp.com/docs/reference/graphql The primary way to interact with the [Accounting Core](/docs/accounting-core) is through the GraphQL API. Use this reference to learn about the full GraphQL Schema. - [Queries](/docs/reference/graphql/queries): Retrieve data from the system. - [Mutations](/docs/reference/graphql/mutations): Modify or create data on the system. - [Directives](/docs/reference/graphql/directives): Add metadata and modify behavior of operations. - [Object Types](/docs/reference/graphql/types/object): The primary data model for responses. - [Input Types](/docs/reference/graphql/types/input): Arguments for mutations and queries. - [Enum Types](/docs/reference/graphql/types/enum): Set of predefined values for a field. - [Scalar Types](/docs/reference/graphql/types/scalar): Primitive values for fields. - [Interface Types](/docs/reference/graphql/types/interface): Set of fields that object types can implement. - [Union Types](/docs/reference/graphql/types/union): Combinations of two or more object types. --- # Mutations Mutations modify or create data on the ledger or execute admin actions by specifying the operation to be performed and the input data. Source: https://www.twisp.com/docs/reference/graphql/mutations ## ach ### createConfiguration Create a configuration for processing an ACH file. Defines settlement exception and suspense accounts. Defines the endpoint that decisioning webhooks are sent to for this configuration. #### Resolves to [`AchConfiguration`](/docs/reference/graphql/types/object#ach-configuration) #### Arguments --- * ``input`` - [`AchCreateConfigurationInput!`](/docs/reference/graphql/types/input#ach-create-configuration-input) * **Request** ```graphql mutation CreateConfiguration { ach { createConfiguration( input: { configId: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" exceptionAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" feeAccountId: "62808a87-0b11-4dce-8877-767b70f029af" journalId: "00000000-0000-0000-0000-000000000000" odfiHeaderConfiguration: { immediateDestination: "026009593" immediateDestinationName: "ACME BANK" immediateOrigin: "111000173" immediateOriginName: "ZUZU" } timeZone: "America/Los_Angeles" } ) { configId } } } ``` **Response** ```json { "data": { "ach": { "createConfiguration": { "configId": "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" } } } } ``` ### generateFile Generate an ACH file. Currently only generates RDFI return files. #### Resolves to [`AchGeneratedFile!`](/docs/reference/graphql/types/object#ach-generated-file) #### Arguments --- * ``input`` - [`AchGenerateFileInput!`](/docs/reference/graphql/types/input#ach-generate-file-input) * **Request** ```graphql mutation GenerateFile { ach { generateFile( input: { configId: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" fileKey: "return.ach" fileType: RDFI_RETURN } ) { fileKey } } } ``` **Response** ```json { "data": { "ach": { "generateFile": { "fileKey": "return.ach" } } } } ``` ### processFile Process an ACH file at the file key with the defined configuration. #### Resolves to [`AchProcessedFile!`](/docs/reference/graphql/types/object#ach-processed-file) #### Arguments --- * ``input`` - [`AchProcessFileInput!`](/docs/reference/graphql/types/input#ach-process-file-input) * **Request** ```graphql mutation ProcessFile { ach { processFile( input: { configId: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" fileKey: "ppd-credit.ach" fileType: RDFI } ) { fileId } } } ``` **Response** ```json { "data": { "ach": { "processFile": { "fileId": "b0d76a9a-afcd-4b1b-b115-01b88d7b84e6" } } } } ``` ### updateConfiguration Update an ACH configuration. #### Resolves to [`AchConfiguration`](/docs/reference/graphql/types/object#ach-configuration) #### Arguments --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``input`` - [`AchUpdateConfigurationInput!`](/docs/reference/graphql/types/input#ach-update-configuration-input) * **Request** ```graphql mutation UpdateConfiguration { ach { updateConfiguration( configId: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" input: { timeZone: "America/Chicago" } ) { configId timeZone version } } } ``` **Response** ```json { "data": { "ach": { "updateConfiguration": { "configId": "1dc71d60-f463-4bb6-b82a-ab42e2f923ff", "timeZone": "America/Chicago", "version": 2 } } } } ``` ## addLimitToControl Add a limit to the control. #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The control to add limit to. --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The velocity limit to add. ## addToAccountSet Add a new member to a set. Members can be an Account or another AccountSet. #### Resolves to [`AccountSet!`](/docs/reference/graphql/types/object#account-set) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for set. --- * ``member`` - [`AccountSetMemberInput!`](/docs/reference/graphql/types/input#account-set-member-input) * Account or AccountSet to add as a member of this set. **Request** ```graphql mutation AddToAccountSet( $accountSetCustomersId: UUID! $accountCustomerAliciaId: UUID! ) { addToAccountSet( id: $accountSetCustomersId member: { memberId: $accountCustomerAliciaId, memberType: ACCOUNT } ) { accountSetId members(first: 10) { nodes { ... on Account { accountId name code } } } } } ``` **Response** ```json { "data": { "addToAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "members": { "nodes": [ { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "name": "Alicia", "code": "CUST.Alicia" } ] } } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c" } ``` ## admin Mutations in the `admin` namespace are used to manage the organization data like users, groups, and tenants. ### createGroup Create a new group. #### Resolves to [`Group`](/docs/reference/graphql/types/object#group) #### Arguments --- * ``input`` - [`CreateGroupInput!`](/docs/reference/graphql/types/input#create-group-input) * Fields to create a new group. **Request** ```graphql mutation AdminCreateGroup { admin { createGroup( input: { id: "917e123a-b89d-4ab5-b11c-cdf6aac80b63" name: "empty-policy-1" description: "A group with an empty policy" policy: "[]" } ) { name description policy } } } ``` **Response** ```json { "data": { "admin": { "createGroup": { "name": "empty-policy-1", "description": "A group with an empty policy", "policy": "[]" } } } } ``` ### createTenant Create a new tenant. #### Resolves to [`Tenant`](/docs/reference/graphql/types/object#tenant) #### Arguments --- * ``input`` - [`CreateTenantInput!`](/docs/reference/graphql/types/input#create-tenant-input) * Fields to create a new tenant. **Request** ```graphql mutation AdminCreateTenant { admin { createTenant( input: { id: "72a0097f-239e-48ec-a417-49c318332ed6" accountId: "sandbox" name: "Sandbox" description: "Sandbox tenant for testing" } ) { accountId name } } } ``` **Response** ```json { "data": { "admin": { "createTenant": { "accountId": "sandbox", "name": "Sandbox" } } } } ``` ### createUser Create a new human user. Upon creation, new users will receive an invite email to sign in to Twisp Console. #### Resolves to [`User`](/docs/reference/graphql/types/object#user) #### Arguments --- * ``input`` - [`CreateUserInput!`](/docs/reference/graphql/types/input#create-user-input) * Fields to create a new user. **Request** ```graphql mutation AdminCreateUser { admin { createUser( input: { id: "9cc8bd28-a36d-502e-89fd-7f1410c1b90a" groupIds: ["d57bd759-73d5-4452-a73e-12b590324e35"] email: "george@twisp.com" } ) { id email groupIds } } } ``` **Response** ```json { "data": { "admin": { "createUser": { "id": "9cc8bd28-a36d-502e-89fd-7f1410c1b90a", "email": "george@twisp.com", "groupIds": ["d57bd759-73d5-4452-a73e-12b590324e35"] } } } } ``` ### deleteGroup Delete an existing group. #### Resolves to [`Group`](/docs/reference/graphql/types/object#group) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name of group to delete. **Request** ```graphql mutation AdminDeleteGroup { admin { deleteGroup(name: "empty-policy-1") { name } } } ``` **Response** ```json { "data": { "admin": { "deleteGroup": { "name": "empty-policy-1" } } } } ``` ### deleteTenant Delete an existing tenant. #### Resolves to [`Tenant`](/docs/reference/graphql/types/object#tenant) #### Arguments --- * ``accountId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier. **Request** ```graphql mutation AdminDeleteTenant { admin { deleteTenant(accountId: "sandbox") { accountId } } } ``` **Response** ```json { "data": { "admin": { "deleteTenant": { "accountId": "sandbox" } } } } ``` ### deleteUser Delete an existing human user. #### Resolves to [`User`](/docs/reference/graphql/types/object#user) #### Arguments --- * ``email`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Email of user to delete. **Request** ```graphql mutation AdminDeleteUser { admin { deleteUser(email: "george@twisp.com") { email } } } ``` **Response** ```json { "data": { "admin": { "deleteUser": { "email": "george@twisp.com" } } } } ``` ### restore Restore a tenant/region into a new tenant/region #### Resolves to [`RestoreOutput`](/docs/reference/graphql/types/object#restore-output) #### Arguments --- * ``input`` - [`RestoreInput!`](/docs/reference/graphql/types/input#restore-input) * ### updateGroup Update an existing group. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Group`](/docs/reference/graphql/types/object#group) #### Arguments --- * ``input`` - [`UpdateGroupInput!`](/docs/reference/graphql/types/input#update-group-input) * Fields to update. **Request** ```graphql mutation AdminUpdateGroup { admin { updateGroup( input: { name: "empty-policy-1" description: "An empty policy layer will default to the base policy." policy: "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"], \"assertions\": {\"always false\": \"1 == 0\"}}]" } ) { name description policy } } } ``` **Response** ```json { "data": { "admin": { "updateGroup": { "name": "empty-policy-1", "description": "An empty policy layer will default to the base policy.", "policy": "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"], \"assertions\": {\"always false\": \"1 == 0\"}}]" } } } } ``` ### updateTenant Update an existing tenant. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Tenant`](/docs/reference/graphql/types/object#tenant) #### Arguments --- * ``input`` - [`UpdateTenantInput!`](/docs/reference/graphql/types/input#update-tenant-input) * Fields to update. **Request** ```graphql mutation AdminUpdateTenant { admin { updateTenant( input: { accountId: "sandbox" description: "This is the sandbox tenant." } ) { accountId description version } } } ``` **Response** ```json { "data": { "admin": { "updateTenant": { "accountId": "sandbox", "description": "This is the sandbox tenant.", "version": 2 } } } } ``` ### updateUser Update an existing human user. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`User`](/docs/reference/graphql/types/object#user) #### Arguments --- * ``input`` - [`UpdateUserInput!`](/docs/reference/graphql/types/input#update-user-input) * Fields to update. **Request** ```graphql mutation AdminUpdateUser { admin { updateUser( input: { email: "george@twisp.com" groupIds: ["152f0c89-6cba-53c9-955e-16d7cbc1f35e"] } ) { email groupIds } } } ``` **Response** ```json { "data": { "admin": { "updateUser": { "email": "george@twisp.com", "groupIds": ["152f0c89-6cba-53c9-955e-16d7cbc1f35e"] } } } } ``` ## attachCalculation Attaches a calculation to an account or account set for entries on the specified journal. #### Resolves to [`AttachedCalculation!`](/docs/reference/graphql/types/object#attached-calculation) #### Arguments --- * ``input`` - [`AttachCalculationInput!`](/docs/reference/graphql/types/input#attach-calculation-input) * **Request** ```graphql mutation AttachCalculation { attachCalculation( input: { accountId: "260fd651-8819-4f99-9c8a-87d27e03ee4c" calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" journalId: "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ) { calculationId } } ``` **Response** ```json { "data": { "attachCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5" } } } ``` ## attachVelocityControl Attach an account or set to the control. #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The control to attach account limit to. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account or Set Id to attach to the velocity control to. --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Journal on which journal to attach the velocity control to. Attaches to the default journal if not provided. --- * ``params`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * The parameters for the velocity limit to use. If attaching for default limits for control (`velocityLimitId == null`), Then params must satify all defined params on defualt limits **Request** ```graphql mutation AttachVelocityControl( $velocityControlId: UUID! = "f6f27892-dfd8-480b-9213-d6adf7ef04ef" $aliciaAccountId: UUID! = "260fd651-8819-4f99-9c8a-87d27e03ee4c" $journalId: UUID! = "822cb59f-ce51-4837-8391-2af3b7a5fc51" $limit: Decimal = "1000.00" ) { attachVelocityControl( velocityControlId: $velocityControlId accountId: $aliciaAccountId journalId: $journalId params: { amount: $limit } ) { velocityControlId } } ``` **Response** ```json { "data": { "attachVelocityControl": { "velocityControlId": "f6f27892-dfd8-480b-9213-d6adf7ef04ef" } } } ``` ## auth Mutations in the `auth` namespace are used to manage clients and their policies. Use the `createClient` mutation to create a new client, `updateClient` to update an existing client, and `deleteClient` to delete a client. ### createClient Create a new security client. #### Resolves to [`Client!`](/docs/reference/graphql/types/object#client) #### Arguments --- * ``input`` - [`CreateClientInput!`](/docs/reference/graphql/types/input#create-client-input) * **Request** ```graphql mutation CreateAuthClient($authClientGithub: String!) { auth { createClient( input: { principal: $authClientGithub name: "Github" policies: [ { effect: ALLOW actions: [SELECT, INSERT, UPDATE, DELETE] resources: ["financial.*"] assertions: { isTrue: "true" } } ] } ) { principal } } } ``` **Response** ```json { "data": { "auth": { "createClient": { "principal": "arn:aws:iam::048962233173:user/github.action" } } } } ``` **Variables** ```json { "authClientGithub": "arn:aws:iam::048962233173:user/github.action" } ``` ### deleteClient Delete a security client. #### Resolves to [`Client`](/docs/reference/graphql/types/object#client) #### Arguments --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Principal to delete. **Request** ```graphql mutation DeleteAuthClient($authClientGithub: String!) { auth { deleteClient(principal: $authClientGithub) { principal } } } ``` **Response** ```json { "data": { "auth": { "deleteClient": { "principal": "arn:aws:iam::048962233173:user/github.action" } } } } ``` **Variables** ```json { "authClientGithub": "arn:aws:iam::048962233173:user/github.action" } ``` ### updateClient Update an existing client by replacing policies. #### Resolves to [`Client`](/docs/reference/graphql/types/object#client) #### Arguments --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Principal of the client to update. --- * ``input`` - [`UpdateClientInput!`](/docs/reference/graphql/types/input#update-client-input) * Client fields to update. **Request** ```graphql mutation UpdateAuthClient($authClientGithub: String!) { auth { updateClient( principal: $authClientGithub input: { policies: [ { effect: ALLOW actions: [SELECT, INSERT] resources: ["financial.*"] assertions: { isTrue: "true" } } ] } ) { policies { effect actions resources assertions } } } } ``` **Response** ```json { "data": { "auth": { "updateClient": { "policies": [ { "effect": "ALLOW", "actions": ["SELECT", "INSERT"], "resources": ["financial.*"], "assertions": { "isTrue": "true" } } ] } } } } ``` **Variables** ```json { "authClientGithub": "arn:aws:iam::048962233173:user/github.action" } ``` ## bulk ### cancelExecution Request cancellation of a running bulk query execution. Returns immediately with `stopping: true` if a cancellation request was sent. Note: there is a race condition between requesting cancellation and the execution completing on its own. The execution may finish (successfully or with an error) before the cancellation takes effect. Use the `execution` query to poll for the final status. #### Resolves to [`CancelBulkQueryExecutionResult`](/docs/reference/graphql/types/object#cancel-bulk-query-execution-result) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ### execute #### Resolves to [`BulkQueryExecution`](/docs/reference/graphql/types/object#bulk-query-execution) #### Arguments --- * ``input`` - [`BulkQueryInput`](/docs/reference/graphql/types/input#bulk-query-input) * ## cards Mutations in the `cards` namespace are used to initialize card processing transaction codes, including specific implementations like the Lithic card processing webhook. ### initializeCardTransactionCodes DEPRECATED: Use Lithic workflow in workflow namespace. Initialize your Twisp instance with Card Processing Transaction Codes. Returns the default settlement account. #### Resolves to [`Account!`](/docs/reference/graphql/types/object#account) #### Arguments --- * ``input`` - [`CardInitializeInput!`](/docs/reference/graphql/types/input#card-initialize-input) * **Request** ```graphql mutation InitializeCardTransactionCodes($journalGLId: UUID!) { cards { initializeCardTransactionCodes(input: { journalId: $journalGLId }) { accountId name code } } } ``` **Response** ```json { "data": { "cards": { "initializeCardTransactionCodes": { "accountId": "f8c3b0a4-e0f7-4057-aced-7030c0eb918a", "name": "Card Settlement Account", "code": "Liabilities.Settlement.Card" } } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ### postLithicTransaction DEPRECATED: Use Lithic workflow in workflow namespace. This mutation supports converting Lithic [Transaction Webhooks JSON](https://docs.lithic.com/docs/transaction-webhooks#schema) into into Twisp accounting core using our card transaction codes. This mutation supports all lithic transaction webhook payloads, including ASA and Balance Inquiry. The general approach is to post all webhooks to Twisp, utilize the balances that come back for decisioning, and allow Twisp and Lithic to work together to track the state of the authorization/settlement cycle. #### Resolves to [`LithicTransactionBalance!`](/docs/reference/graphql/types/object#lithic-transaction-balance) #### Arguments --- * ``input`` - [`LithicTransactionInput!`](/docs/reference/graphql/types/input#lithic-transaction-input) * ## createAccount Create a new account. #### Resolves to [`Account!`](/docs/reference/graphql/types/object#account) #### Arguments --- * ``input`` - [`AccountInput!`](/docs/reference/graphql/types/input#account-input) * Fields to create a new account. **Request** ```graphql mutation CreateAccount($accountCardSettlementId: UUID!) { createAccount( input: { accountId: $accountCardSettlementId name: "Card Settlement" code: "SETTLE.CARD" description: "Settlement account for card transactions." normalBalanceType: CREDIT status: ACTIVE } ) { accountId name code description normalBalanceType } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "name": "Card Settlement", "code": "SETTLE.CARD", "description": "Settlement account for card transactions.", "normalBalanceType": "CREDIT" } } } ``` **Variables** ```json { "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` ## createAccountSet Create a new account set. #### Resolves to [`AccountSet!`](/docs/reference/graphql/types/object#account-set) #### Arguments --- * ``input`` - [`AccountSetInput!`](/docs/reference/graphql/types/input#account-set-input) * Fields to create a new account set. **Request** ```graphql mutation CreateAccountSet($accountSetCustomersId: UUID!, $journalGLId: UUID!) { createAccountSet( input: { accountSetId: $accountSetCustomersId journalId: $journalGLId name: "Customers" description: "All customer wallets." normalBalanceType: DEBIT } ) { accountSetId name description code } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets.", "code": "Ke8_GJexQNmYUifxYHtsqIIstZ_OUUg3g5Eq87el_FE" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` ## createCalculation Create a calculation, which allows for balances to be customized on dimensions/filters. #### Resolves to [`Calculation!`](/docs/reference/graphql/types/object#calculation) #### Arguments --- * ``input`` - [`CreateCalculationInput!`](/docs/reference/graphql/types/input#create-calculation-input) * **Request** ```graphql mutation CreateCalculation { createCalculation( input: { calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" code: "EFFECTIVE_DATE" description: "Track balances per EFFECTIVE_DATE in an account." dimensions: [ { alias: "effectiveDate", value: "context.vars.transaction.effective" } ] scope: LOCAL } ) { calculationId code description scope dimensions { alias value } } } ``` **Response** ```json { "data": { "createCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "code": "EFFECTIVE_DATE", "description": "Track balances per EFFECTIVE_DATE in an account.", "dimensions": [ { "alias": "effectiveDate", "value": "context.vars.transaction.effective" } ], "scope": "LOCAL" } } } ``` ## createJournal Create a new journal for recording transactions in the ledger. #### Resolves to [`Journal!`](/docs/reference/graphql/types/object#journal) #### Arguments --- * ``input`` - [`JournalInput!`](/docs/reference/graphql/types/input#journal-input) * Fields to create a new Journal. **Request** ```graphql mutation CreateJournal($journalGLId: UUID!) { createJournal( input: { journalId: $journalGLId name: "GL" description: "General Ledger" status: ACTIVE } ) { journalId name description status } } ``` **Response** ```json { "data": { "createJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "GL", "description": "General Ledger", "status": "ACTIVE" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## createTranCode Create a new transaction code (tran code). #### Resolves to [`TranCode!`](/docs/reference/graphql/types/object#tran-code) #### Arguments --- * ``input`` - [`TranCodeInput!`](/docs/reference/graphql/types/input#tran-code-input) * Fields to create a new TranCode. **Request** ```graphql mutation CreateTranCode( $tcBookTransferId: UUID! $journalGLIdExp: Expression! ) { createTranCode( input: { tranCodeId: $tcBookTransferId code: "BOOK_TRANSFER" description: "Book transfer between two internal accounts." metadata: { category: "Internal" } params: [ { name: "crAccount", type: UUID, description: "Account to credit." } { name: "drAccount", type: UUID, description: "Account to debit." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "currency" type: STRING description: "Currency used for transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } ] vars: { amount2: "decimal('1.00')", amount3: "this.amount2" } transaction: { journalId: $journalGLIdExp effective: "params.effective" description: "'Book transfer for $' + string(params.amount)" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } } } ``` **Variables** ```json { "journalGLIdExp": "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')", "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` ## createVelocityControl #### Resolves to [`VelocityControl!`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``input`` - [`VelocityControlInput!`](/docs/reference/graphql/types/input#velocity-control-input) * **Request** ```graphql mutation CreateVelocityControl { createVelocityControl( input: { velocityControlId: "f6f27892-dfd8-480b-9213-d6adf7ef04ef" name: "Example Control" description: """ Example control that rejects transactions unless transaction metadata explictly disables enforcement of control. """ condition: "context.vars.transaction.?metadata.disableVelocityControl.orValue(false) == false" enforcement: { action: REJECT } } ) { velocityControlId name } } ``` **Response** ```json { "data": { "createVelocityControl": { "velocityControlId": "f6f27892-dfd8-480b-9213-d6adf7ef04ef", "name": "Example Control" } } } ``` ## createVelocityLimit #### Resolves to [`VelocityLimit!`](/docs/reference/graphql/types/object#velocity-limit) #### Arguments --- * ``input`` - [`VelocityLimitInput!`](/docs/reference/graphql/types/input#velocity-limit-input) * **Request** ```graphql mutation CreateVelocityLimit { createVelocityLimit( input: { velocityLimitId: "370ac19b-a48c-4480-9b1f-18c39d214a0a" name: "Example Monthly Limit" description: """ A per month spending limit at the ENCUMBRANCE layer. """ window: [ { alias: "year", value: "context.vars.transaction.effective.getYear()" } { alias: "month" value: "context.vars.transaction.effective.getMonth()" } ] currency: "USD" limit: { balance: [ { layer: "ENCUMBRANCE" amount: "params.amount" normalBalanceType: "DEBIT" } ] } params: [ { name: "amount" type: DECIMAL description: """ Aggregate spend up to amount per effective month. Defaults to $100.00 """ default: "100.00" } ] # Add to existing velocity control velocityControlIds: ["f6f27892-dfd8-480b-9213-d6adf7ef04ef"] } ) { velocityLimitId } } ``` **Response** ```json { "data": { "createVelocityLimit": { "velocityLimitId": "370ac19b-a48c-4480-9b1f-18c39d214a0a" } } } ``` ## deleteAccount Delete account moves the account state to `LOCKED`. When an account is in LOCKED, prevents entries from being posted to it. #### Resolves to [`Account`](/docs/reference/graphql/types/object#account) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql mutation DeleteAccount($accountCustomerBobbyId: UUID!) { deleteAccount(id: $accountCustomerBobbyId) { accountId status } } ``` **Response** ```json { "data": { "deleteAccount": { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "status": "LOCKED" } } } ``` **Variables** ```json { "accountCustomerBobbyId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" } ``` ## deleteAccountSet Delete an account set. #### Resolves to [`AccountSet`](/docs/reference/graphql/types/object#account-set) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql mutation DeleteAccountSet($accountSetCustomersId: UUID!) { deleteAccountSet(id: $accountSetCustomersId) { accountSetId } } ``` **Response** ```json { "data": { "deleteAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` ## deleteCalculation " Delete calculation sets calculation to `LOCKED`. If in attached scope, must remove all attachments first. #### Resolves to [`Calculation`](/docs/reference/graphql/types/object#calculation) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql mutation DeleteCalculation { deleteCalculation(id: "5867b5dd-fc69-416c-80f5-62e8a53610d5") { calculationId status } } ``` **Response** ```json { "data": { "deleteCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "status": "LOCKED" } } } ``` ## deleteJournal Moves journal into `LOCKED` status. Prevents entries from being posted to the journal. #### Resolves to [`Journal`](/docs/reference/graphql/types/object#journal) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql mutation DeleteJournal($journalGLId: UUID!) { deleteJournal(id: $journalGLId) { journalId status } } ``` **Response** ```json { "data": { "deleteJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "status": "LOCKED" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## deleteTranCode Moves the tran code into `LOCKED` status. Prevents transactions from posting using this version of tran code. #### Resolves to [`TranCode`](/docs/reference/graphql/types/object#tran-code) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql mutation DeleteTranCode($tcBookTransferId: UUID!) { deleteTranCode(id: $tcBookTransferId) { tranCodeId status } } ``` **Response** ```json { "data": { "deleteTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855", "status": "LOCKED" } } } ``` **Variables** ```json { "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` ## deleteVelocityControl #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ## deleteVelocityLimit #### Resolves to [`VelocityLimit`](/docs/reference/graphql/types/object#velocity-limit) #### Arguments --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ## detachCalculation Removes calculation from attachment. #### Resolves to [`AttachedCalculation`](/docs/reference/graphql/types/object#attached-calculation) #### Arguments --- * ``input`` - [`DetachCalculationInput!`](/docs/reference/graphql/types/input#detach-calculation-input) * **Request** ```graphql mutation DetachCalculation { detachCalculation( input: { accountId: "260fd651-8819-4f99-9c8a-87d27e03ee4c" calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" journalId: "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ) { calculationId } } ``` **Response** ```json { "data": { "detachCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5" } } } ``` ## detachVelocityControl detach account from control. #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The Velocity Control to detach. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The account id detaching from. --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * The journal detaching from. If not provided this will detach from default journal. ## evaluate Evaluate a CEL (common expression language) expression using Twisp's calculation engine. Returns a String representation of the evaluated result. #### Resolves to [`Value!`](/docs/reference/graphql/types/scalar#value) #### Arguments --- * ``expressions`` - [`ExpressionNestedMap!`](/docs/reference/graphql/types/scalar#expression-nested-map) * A nested map of literal CEL expressions to be evaluated in a shared context Ex: { "two": "this.one + 1", "one": "2 - 1", "sqrt2": "math.Sqrt(double(2))", "now": "time.Now()", "obj": { "foo": "'bar'" } } --- * ``document`` - [`Value`](/docs/reference/graphql/types/scalar#value) * Value object. Similar to the JSON object with support for type coersion **Request** ```graphql mutation Cel( $id: String! = "uuid.New()" @cel $uppercase: String! = "strings.ToUpper(context.vars.lowercase)" @cel $expression: String! @cel $this: String! = "this.uppercase" @cel $nonCEL: String! $celNonCEL: String! = "this.nonCEL" @cel ) { evaluate( expressions: { a: "int(1)" b: "document.greeting + ' World!'" c: "decimal.Sub(decimal('1.234567'), decimal('0.987654321'))" d: "math.Max(double(123), double(456))" e: "size(string(document.uuid))" f: "document.upper" g: "document.expr" h: "document.this" i: "strings.ToUpper(document.celNonCEL)" j: "context.auth" k: "decimal('-inf')" l: "string(decimal('-inf'))" m: { n: "document.this" } o: "this.m.n" } document: { greeting: "Hello" uuid: $id upper: $uppercase expr: $expression this: $this nonCEL: $nonCEL celNonCEL: $celNonCEL } ) } ``` **Response** ```json { "data": { "evaluate": { "a": 1, "b": "Hello World!", "c": "0.246912679", "d": 456, "e": 36, "f": "JOHN DOE", "g": 4, "h": "JOHN DOE", "i": "NOT CEL", "j": { "claims": {}, "policies": [ { "actions": ["*"], "effect": 1, "resources": ["*"] } ], "principal": "test" }, "k": "-Infinity", "l": "-Infinity", "m": { "n": "JOHN DOE" }, "o": "JOHN DOE" } } } ``` **Variables** ```json { "lowercase": "john doe", "expression": "2 * 2", "nonCEL": "not cel" } ``` ## events Queries in the `events` namespace retrieve event data, such as webhooks. ### createEndpoint Create a new endpoint. #### Resolves to [`Endpoint!`](/docs/reference/graphql/types/object#endpoint) #### Arguments --- * ``input`` - [`EndpointInput!`](/docs/reference/graphql/types/input#endpoint-input) * Fields to create a new endpoint. **Request** ```graphql mutation CreateEndpoint { events { createEndpoint( input: { endpointId: "345940ed-2726-4b20-88aa-820857ac0e68" status: ENABLED endpointType: WEBHOOK url: "https://webhook.site/twisp-webhook-test" description: "subscribe to balance and account events" subscription: ["balance*", "account.*"] } ) { endpointId status endpointType url subscription description } } } ``` **Response** ```json { "data": { "events": { "createEndpoint": { "endpointId": "345940ed-2726-4b20-88aa-820857ac0e68", "status": "ENABLED", "endpointType": "WEBHOOK", "url": "https://webhook.site/twisp-webhook-test", "subscription": ["balance*", "account.*"], "description": "subscribe to balance and account events" } } } } ``` ### deleteEndpoint #### Resolves to [`Endpoint`](/docs/reference/graphql/types/object#endpoint) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql mutation DeleteEndpoint { events { deleteEndpoint(id: "345940ed-2726-4b20-88aa-820857ac0e68") { endpointId status endpointType url subscription description } } } ``` **Response** ```json { "data": { "events": { "deleteEndpoint": { "endpointId": "345940ed-2726-4b20-88aa-820857ac0e68", "status": "DISABLED", "endpointType": "WEBHOOK", "url": "https://webhook.site/updated-webhook-test", "subscription": ["account.*"], "description": "updated status" } } } } ``` ### updateEndpoint Update fields on an existing account. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Endpoint`](/docs/reference/graphql/types/object#endpoint) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`EndpointUpdateInput!`](/docs/reference/graphql/types/input#endpoint-update-input) * Fields to update. **Request** ```graphql mutation UpdateEndpoint { events { updateEndpoint( id: "345940ed-2726-4b20-88aa-820857ac0e68" input: { status: DISABLED url: "https://webhook.site/updated-webhook-test" description: "updated status" subscription: ["account.*"] filters: { isTrue: "true" } } ) { endpointId status endpointType url subscription description filters } } } ``` **Response** ```json { "data": { "events": { "updateEndpoint": { "endpointId": "345940ed-2726-4b20-88aa-820857ac0e68", "status": "DISABLED", "endpointType": "WEBHOOK", "url": "https://webhook.site/updated-webhook-test", "subscription": ["account.*"], "description": "updated status", "filters": { "isTrue": "true" } } } } } ``` ## files ### createDownload Create a link to download a file. #### Resolves to [`Download`](/docs/reference/graphql/types/object#download) #### Arguments --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. **Request** ```graphql mutation CreateDownload { files { createDownload(key: "some-file.json") { downloadURL } } } ``` **Response** ```json { "data": { "files": { "createDownload": { "downloadURL": "http://localhost:8080/files?tenant=REDACTED&key=some-file.json" } } } } ``` ### createUpload Create a file upload. #### Resolves to [`Upload`](/docs/reference/graphql/types/object#upload) #### Arguments --- * ``input`` - [`CreateUpload!`](/docs/reference/graphql/types/input#create-upload) * ## iso8583 ### createConfig Create a configuration for ISO8583 processing. Defines settlement account, journal, and timezone. #### Resolves to [`ISO8583Config`](/docs/reference/graphql/types/object#iso8583config) #### Arguments --- * ``input`` - [`ISO8583CreateConfigInput!`](/docs/reference/graphql/types/input#iso8583create-config-input) * **Request** ```graphql # Create a journal and settlement account first before creating config mutation CreateISO8583Config { # Create the ISO8583 config iso8583 { createConfig( input: { configId: "670504f6-0515-11f0-a441-069b540ea27c" journalId: "8b65c8dc-0515-11f0-a441-069b540ea27c" settlementAccountId: "779886bc-0515-11f0-aa78-069b540ea27c" timeZone: "America/Chicago" description: "Primary ISO8583 processor configuration" processor: I2C } ) { configId journalId settlementAccountId timeZone description processor version } } } ``` **Response** ```json { "data": { "iso8583": { "createConfig": { "configId": "670504f6-0515-11f0-a441-069b540ea27c", "journalId": "8b65c8dc-0515-11f0-a441-069b540ea27c", "settlementAccountId": "779886bc-0515-11f0-aa78-069b540ea27c", "timeZone": "America/Chicago", "description": "Primary ISO8583 processor configuration", "processor": "I2C", "version": 1 } } } } ``` ### deleteConfig Remove configuration. #### Resolves to [`ISO8583Config`](/docs/reference/graphql/types/object#iso8583config) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ### updateConfig Update configuration for ISO8583 processing. #### Resolves to [`ISO8583Config`](/docs/reference/graphql/types/object#iso8583config) #### Arguments --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``input`` - [`ISO8583UpdateConfigInput!`](/docs/reference/graphql/types/input#iso8583update-config-input) * ## kv Namespace for transactional key/value mutations. ### delete Delete a KV record by its `(namespace, key)` natural key. Optional `conditions` run against the current record, with `document` and `value` bound to it — or `null` when no record exists. The delete proceeds only if all conditions evaluate to `true`. #### Resolves to [`KVValue`](/docs/reference/graphql/types/object#kvvalue) #### Arguments --- * ``namespace`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``conditions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * A map of literal CEL expressions to be evaluated in a shared context Ex: { "two": "this.one + 1", "one": "2 - 1", "sqrt2": "math.Sqrt(double(2))", "now": "time.Now()" } **Request** ```graphql mutation DeleteKv { kv { delete(namespace: "flags", key: "feature-a") { namespace key description value } } } ``` **Response** ```json { "data": { "kv": { "delete": { "namespace": "flags", "key": "feature-a", "description": "feature flag a", "value": { "enabled": false, "rollout": 50, "owner": "risk" } } } } } ``` ### put Create or replace a KV record. Writes a new version on every call. Limits: - `namespace`: max 512 UTF-8 bytes - `key`: max 512 UTF-8 bytes - persisted payload (`description` + serialized `value`): max 256 KiB #### Resolves to [`KVValue!`](/docs/reference/graphql/types/object#kvvalue) #### Arguments --- * ``input`` - [`KVPutInput!`](/docs/reference/graphql/types/input#kvput-input) * **Request** ```graphql mutation PutKv { kv { put( input: { namespace: "flags" key: "feature-a" description: "feature flag a" value: { enabled: true, rollout: 25, owner: "risk" } } ) { namespace key description value version } } } ``` **Response** ```json { "data": { "kv": { "put": { "namespace": "flags", "key": "feature-a", "description": "feature flag a", "value": { "enabled": true, "rollout": 25, "owner": "risk" }, "version": 1 } } } } ``` ### update Evaluate an expression value against the existing KV `value` and merge the result in using RFC 7396 semantics. String leaves are CEL expressions; non-string leaves are literal patch values. `value` and `document` in the expression context both refer to the existing KV `value`. Fails with NOT_FOUND when no record exists for `(namespace, key)`. #### Resolves to [`KVValue!`](/docs/reference/graphql/types/object#kvvalue) #### Arguments --- * ``input`` - [`KVUpdateInput!`](/docs/reference/graphql/types/input#kvupdate-input) * **Request** ```graphql mutation UpdateKv { kv { update( input: { namespace: "flags" key: "feature-a" expressions: { enabled: false, rollout: "value.rollout + 25" } conditions: { current_rollout: "document.value.rollout == 25" } } ) { namespace key description value version conditions } } } ``` **Response** ```json { "data": { "kv": { "update": { "namespace": "flags", "key": "feature-a", "description": "feature flag a", "value": { "enabled": false, "rollout": 50, "owner": "risk" }, "version": 2, "conditions": { "current_rollout": "document.value.rollout == 25" } } } } } ``` ## postTransaction Write a transaction to the ledger using the predefined defaults from the `tranCode` provided. #### Resolves to [`Transaction!`](/docs/reference/graphql/types/object#transaction) #### Arguments --- * ``input`` - [`TransactionInput!`](/docs/reference/graphql/types/input#transaction-input) * Fields to post a new Transaction. **Request** ```graphql mutation PostTransaction( $accountCustomerAliciaId: UUID! $accountCustomerBobbyId: UUID! ) { postTransaction( input: { transactionId: "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" tranCode: "BOOK_TRANSFER" params: { crAccount: $accountCustomerAliciaId drAccount: $accountCustomerBobbyId amount: "1.00" currency: "USD" effective: "2022-09-08" } } ) { transactionId } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" } } } ``` **Variables** ```json { "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "accountCustomerBobbyId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" } ``` ## queries #### Resolves to [`Query!`](/docs/reference/graphql/types/object#query) #### Arguments ## removeAllLimitsFromControl Remove all limits from the velocity control. #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The control to remove from. ## removeFromAccountSet Remove a member from a set. #### Resolves to [`AccountSet!`](/docs/reference/graphql/types/object#account-set) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for set. --- * ``member`` - [`AccountSetMemberInput!`](/docs/reference/graphql/types/input#account-set-member-input) * Account or AccountSet to add as a member of this set. ## removeLimitFromControl Remove a single limit from control. #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The control to remove from. --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The velocity limit to remove. ## scheduler Mutations in the `scheduler` namespace are used to manage scheduled jobs ### createSchedule #### Resolves to [`Schedule`](/docs/reference/graphql/types/object#schedule) #### Arguments --- * ``input`` - [`CreateScheduleInput!`](/docs/reference/graphql/types/input#create-schedule-input) * Fields to create a new schedule. ## schema Mutations in the `schema` namespace are used to manage custom indexes, aggregates, and historical indexes. Use the `schema` namespace to create and delete indexes and aggregates. ### createHistoricalIndex Create a custom index for querying all versions of records' histories. Because the Twisp FLDB is an [immutable, append-only data store](https://www.twisp.com/docs/infrastructure/ledger-database#append-only-immutability), changing the data within any record results in a new version of that record, leaving the original version intact in history. While regular indexes (such as those created by the `schema.createIndex` mutation) will index the most recent version of records, a historical index will index every version. This allows for defining sophisticated indexes to enable queries such as: - Retrieving account balances when the balance amount was negative. - Finding records' state when a particular value is set in their metadata. - Pulling time-delimited sets of balance activity. Partitions on historical indexes will by definition be larger than their equivalent partitions for regular indexes as they will contain not just the latest version of a record but also every previous version. #### Resolves to [`Index!`](/docs/reference/graphql/types/object#index) #### Arguments --- * ``input`` - [`CreateIndexInput!`](/docs/reference/graphql/types/input#create-index-input) * ### createIndex Create a custom index for querying records. Currently available for indexing Account, AccountSet, Balance, Entry, Transaction, and TranCode record types. To query the index, use the `CUSTOM` index type for the applicable resource query and supply the filter inputs specified by the index. Custom indexes can be created using fields on the root level of the record like `Account.modified` as well as nested fields within documents like the `metadata` object. Depending on the parameters defined, custom indexes may be structured to return a single record or a sorted list of records. Note that due to the scaling properties of the underlying database, a single partition supports a fixed amount of read bandwidth and individual write operations per second. Beyond that threshold, throttling will occur. Visit scaling properties for more information. When designing custom indexes, care must be taken to ensure that reads and writes are spread across a sufficient number of partitions to support peak workloads. In practice, partitioning by account is usually sufficient. Our technical support staff is available for guidance on partition design patterns at [support@twisp.com](mailto:support@twisp.com). To learn more about indexes within the Twisp FLDB, see [Index-First Design](https://www.twisp.com/docs/infrastructure/ledger-database#index-first-design) in the docs. #### Resolves to [`Index!`](/docs/reference/graphql/types/object#index) #### Arguments --- * ``input`` - [`CreateIndexInput!`](/docs/reference/graphql/types/input#create-index-input) * **Request** ```graphql mutation SchemaCreateIndex { schema { createIndex( input: { name: "Transaction.metadata.category" on: Transaction unique: false partition: [ { alias: "correlation_id", value: "document.correlation_id" } ] sort: [ { alias: "category" value: "string(document.metadata.category)" sort: ASC } ] constraints: { hasCategory: "has(document.metadata.category)" } } ) { range { alias value sort } partition { alias value } on constraints unique } } } ``` **Response** ```json { "data": { "schema": { "createIndex": { "range": [ { "alias": "category", "value": "string(document.metadata.category)", "sort": "ASC" } ], "partition": [ { "alias": "correlation_id", "value": "document.correlation_id" } ], "on": "Transaction", "constraints": { "hasCategory": "has(document.metadata.category)" }, "unique": false } } } } ``` ### createSearchIndex Create a search index for full text search support. Full text search indexes are powered by opensearch. These indexes are eventually consistent (populated by the async materializer after the source write commits), but have the ability to execute complex queries utilizing the opensearch indexing engine. An explicit `opensearchSchema` is required — it declares the Opensearch field types for the indexed documents and keeps sort/filter/cursor behavior stable. #### Resolves to [`Index!`](/docs/reference/graphql/types/object#index) #### Arguments --- * ``input`` - [`CreateSearchIndexInput!`](/docs/reference/graphql/types/input#create-search-index-input) * ### createView Create an view for a set of source tables. Views are materialized views that are defined by a CEL-expression map and get triggered by changes to one or more source tables. They allow for creating denormalized data structures that are automatically updated whenever source data changes. Each view has: - A document schema defined as a map of CEL expressions - Source table(s) that trigger updates to the view - Optional dimension(s) for partitioning and uniqueness constraints - Optional filters that determine when the view should be updated When source data changes, the view is automatically recalculated based on the provided CEL expressions. #### Resolves to [`View!`](/docs/reference/graphql/types/object#view) #### Arguments --- * ``input`` - [`CreateViewInput!`](/docs/reference/graphql/types/input#create-view-input) * Input for creating a new view materialized view. ### deleteIndex Delete an existing index. When `on: View`, `viewName` must be supplied to identify the target view. #### Resolves to [`Index`](/docs/reference/graphql/types/object#index) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``on`` - [`IndexOnEnum!`](/docs/reference/graphql/types/enum#index-on-enum) * Record types which support custom indexes. --- * ``viewName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. **Request** ```graphql mutation SchemaDeleteIndex { schema { deleteIndex(name: "Transaction.metadata.category", on: Transaction) { name on } } } ``` **Response** ```json { "data": { "schema": { "deleteIndex": { "name": "Transaction.metadata.category", "on": "Transaction" } } } } ``` ### deleteView Delete an existing view. #### Resolves to [`View`](/docs/reference/graphql/types/object#view) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ### updateSearchIndex Update a search index #### Resolves to [`Index`](/docs/reference/graphql/types/object#index) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``on`` - [`IndexOnEnum!`](/docs/reference/graphql/types/enum#index-on-enum) * Record types which support custom indexes. --- * ``opensearchSchema`` - [`OpensearchSchemaInput!`](/docs/reference/graphql/types/input#opensearch-schema-input) * ## updateAccount Update fields on an existing account. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Account!`](/docs/reference/graphql/types/object#account) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`AccountUpdateInput!`](/docs/reference/graphql/types/input#account-update-input) * Fields to update. **Request** ```graphql mutation UpdateAccount($accountCardSettlementId: UUID!) { updateAccount(id: $accountCardSettlementId, input: { code: "CARD.SETTLE" }) { accountId code history(first: 2) { nodes { version code } } } } ``` **Response** ```json { "data": { "updateAccount": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "code": "CARD.SETTLE", "history": { "nodes": [ { "version": 2, "code": "CARD.SETTLE" }, { "version": 1, "code": "SETTLE.CARD" } ] } } } } ``` **Variables** ```json { "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` ## updateAccountSet Update fields on an existing account set. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`AccountSet!`](/docs/reference/graphql/types/object#account-set) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`AccountSetUpdateInput!`](/docs/reference/graphql/types/input#account-set-update-input) * Fields to update. **Request** ```graphql mutation UpdateAccountSet($accountSetCustomersId: UUID!) { updateAccountSet( id: $accountSetCustomersId input: { name: "Customer Wallets", code: "CUSTOMERS.WALLETS" } ) { accountSetId name code history(first: 2) { nodes { version name code } } } } ``` **Response** ```json { "data": { "updateAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customer Wallets", "code": "CUSTOMERS.WALLETS", "history": { "nodes": [ { "version": 3, "name": "Customer Wallets", "code": "CUSTOMERS.WALLETS" }, { "version": 2, "name": "Customers", "code": "Ke8_GJexQNmYUifxYHtsqIIstZ_OUUg3g5Eq87el_FE" } ] } } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` ## updateCalculation Update fields on an existing calculation. #### Resolves to [`Calculation!`](/docs/reference/graphql/types/object#calculation) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`CalculationUpdateInput!`](/docs/reference/graphql/types/input#calculation-update-input) * Fields to update. **Request** ```graphql mutation UpdateCalculation { updateCalculation( id: "5867b5dd-fc69-416c-80f5-62e8a53610d5" input: { code: "EFFECTIVE_DATE_LOCAL" description: "Track balances per EFFECTIVE_DATE locally in an account." } ) { calculationId code description } } ``` **Response** ```json { "data": { "updateCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "code": "EFFECTIVE_DATE_LOCAL", "description": "Track balances per EFFECTIVE_DATE locally in an account." } } } ``` ## updateEntry Update an existing ledger entry. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Entry!`](/docs/reference/graphql/types/object#entry) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`EntryUpdateInput!`](/docs/reference/graphql/types/input#entry-update-input) * Entry fields to update. ## updateJournal Update an existing journal. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Journal!`](/docs/reference/graphql/types/object#journal) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`JournalUpdateInput!`](/docs/reference/graphql/types/input#journal-update-input) * Journal fields to update. **Request** ```graphql mutation UpdateJournal($journalGLId: UUID!) { updateJournal( id: $journalGLId input: { description: "_The_ ledger. The only one." } ) { journalId description history(first: 2) { nodes { version description } } } } ``` **Response** ```json { "data": { "updateJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "description": "_The_ ledger. The only one.", "history": { "nodes": [ { "version": 2, "description": "_The_ ledger. The only one." }, { "version": 1, "description": "General Ledger" } ] } } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## updateTranCode Update an existing tran code. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`TranCode!`](/docs/reference/graphql/types/object#tran-code) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`TranCodeUpdateInput!`](/docs/reference/graphql/types/input#tran-code-update-input) * TranCode fields to update. **Request** ```graphql mutation UpdateTranCode($tcBookTransferId: UUID!) { updateTranCode( id: $tcBookTransferId input: { description: "Book transfer between two customer wallet accounts." vars: { hello: "string('world')" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" metadata: "{'first': 1}" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" metadata: "{'second':2}" } ] } ) { tranCodeId description entries { metadata } vars history(first: 2) { nodes { version description vars } } } } ``` **Response** ```json { "data": { "updateTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855", "description": "Book transfer between two customer wallet accounts.", "entries": [ { "metadata": "{'first': 1}" }, { "metadata": "{'second':2}" } ], "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2", "hello": "string('world')" }, "history": { "nodes": [ { "version": 2, "description": "Book transfer between two customer wallet accounts.", "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2", "hello": "string('world')" } }, { "version": 1, "description": "Book transfer between two internal accounts.", "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2" } } ] } } } } ``` **Variables** ```json { "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` ## updateTransaction Update an existing transaction. To ensure data integrity, only a subset of fields are allowed. #### Resolves to [`Transaction!`](/docs/reference/graphql/types/object#transaction) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``input`` - [`TransactionUpdateInput!`](/docs/reference/graphql/types/input#transaction-update-input) * Transaction fields to update. **Request** ```graphql mutation UpdateTransaction { updateTransaction( id: "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" input: { metadata: { reconciled: true } } ) { transactionId metadata history(first: 2) { nodes { version metadata } } } } ``` **Response** ```json { "data": { "updateTransaction": { "transactionId": "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68", "metadata": { "reconciled": true }, "history": { "nodes": [ { "version": 2, "metadata": { "reconciled": true } }, { "version": 1, "metadata": {} } ] } } } } ``` ## updateVelocityControl #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``input`` - [`UpdateVelocityControlInput!`](/docs/reference/graphql/types/input#update-velocity-control-input) * ## updateVelocityLimit #### Resolves to [`VelocityLimit`](/docs/reference/graphql/types/object#velocity-limit) #### Arguments --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``input`` - [`UpdateVelocityLimitInput!`](/docs/reference/graphql/types/input#update-velocity-limit-input) * ## voidTransaction Void an existing transaction. #### Resolves to [`Transaction`](/docs/reference/graphql/types/object#transaction) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. --- * ``properties`` - [`VoidTransactionPropertiesInput`](/docs/reference/graphql/types/input#void-transaction-properties-input) * **Request** ```graphql mutation VoidTransaction($transactionId: UUID!) { voidTransaction(id: $transactionId) { transactionId voidOf } } ``` **Response** ```json { "data": { "voidTransaction": { "transactionId": "61a31b31-51d3-54e1-8420-f2c0d953adc5", "voidOf": "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" } } } ``` **Variables** ```json { "transactionId": "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" } ``` ## warehouse Mutations in the `warehouse` namespace are used to manage the twisp data warehouse. ### batchExecuteStatement #### Resolves to [`BatchExecuteStatementOutput`](/docs/reference/graphql/types/object#batch-execute-statement-output) #### Arguments --- * ``input`` - [`BatchExecuteStatementInput!`](/docs/reference/graphql/types/input#batch-execute-statement-input) * ### cancelStatement #### Resolves to [`CancelStatementOutput`](/docs/reference/graphql/types/object#cancel-statement-output) #### Arguments --- * ``input`` - [`CancelStatementInput!`](/docs/reference/graphql/types/input#cancel-statement-input) * ### executeStatement #### Resolves to [`ExecuteStatementOutput`](/docs/reference/graphql/types/object#execute-statement-output) #### Arguments --- * ``input`` - [`ExecuteStatementInput!`](/docs/reference/graphql/types/input#execute-statement-input) * ### executeStatementSync #### Resolves to [`ExecuteStatementSyncOutput`](/docs/reference/graphql/types/object#execute-statement-sync-output) #### Arguments --- * ``input`` - [`ExecuteStatementSyncInput!`](/docs/reference/graphql/types/input#execute-statement-sync-input) * **Request** ```graphql mutation uuid_formatting { warehouse { executeStatementSync( input: { sql: "select accountid, from_varbyte(accountid, 'hex') as uuid from account_history limit 1" } ) { records { fields { value { str } } } columnMetadata { name } } } } ``` **Response** ```json { "data": { "warehouse": { "executeStatementSync": { "records": [ { "fields": [ { "value": { "str": "6BEVvBrRTiGFH4MlyyzHTQ==" } }, { "value": { "str": "e81115bc1ad14e21851f8325cb2cc74d" } } ] } ], "columnMetadata": [ { "name": "accountid" }, { "name": "uuid" } ] } } } } ``` ### export #### Resolves to [`Export!`](/docs/reference/graphql/types/object#export) #### Arguments --- * ``input`` - [`ExportInput!`](/docs/reference/graphql/types/input#export-input) * ## workflow Mutations in the `workflow` namespace are used to manage and execute workflows. ### execute Execute a workflow identified by `workflowId`. #### Resolves to [`WorkflowExecution!`](/docs/reference/graphql/types/object#workflow-execution) #### Arguments --- * ``input`` - [`WorkflowInput!`](/docs/reference/graphql/types/input#workflow-input) * Fields to execute a new workflow. ### executeTask Execution workflow identified by `workflowId` and `executionId` to the state identified by `task`. #### Resolves to [`WorkflowExecution!`](/docs/reference/graphql/types/object#workflow-execution) #### Arguments --- * ``input`` - [`WorkflowInput!`](/docs/reference/graphql/types/input#workflow-input) * Fields to execute a new workflow. --- # Queries Queries retrieve data from the system by specifying the fields to be returned. Source: https://www.twisp.com/docs/reference/graphql/queries ## account Get a single account by its `accountId`. #### Resolves to [`Account`](/docs/reference/graphql/types/object#account) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. _Example:_ ``"3ea12e45-7df2-4293-9434-feb792affc91"`` **Request** ```graphql query GetAccount { account(id: "a9c8dde6-c0e5-407c-9d99-029c523f7ea8") { accountId code name description normalBalanceType status } } ``` **Response** ```json { "data": { "account": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "code": "SETTLE.CARD", "name": "Card Settlement", "description": "Settlement account for card transactions.", "normalBalanceType": "CREDIT", "status": "ACTIVE" } } } ``` ## accountSet Get a single account set by its `accountSetId`. #### Resolves to [`AccountSet`](/docs/reference/graphql/types/object#account-set) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql query GetAccountSet { accountSet(id: "29ef3f18-97b1-40d9-9852-27f1607b6ca8") { accountSetId name description members(first: 10) { nodes { ... on Account { accountId code name } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets.", "members": { "nodes": [ { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "code": "CUST.Bobby", "name": "Bobby" }, { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "code": "CUST.Alicia", "name": "Alicia" } ] } } } } ``` ## accountSets Select one or more account sets. Specify the index to use and apply filters to your query. #### Resolves to [`AccountSetConnection!`](/docs/reference/graphql/types/object#account-set-connection) #### Arguments --- * ``index`` - [`AccountSetIndexInput!`](/docs/reference/graphql/types/input#account-set-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`AccountSetFilterInput`](/docs/reference/graphql/types/input#account-set-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of sets to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve sets. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetAccountSets { accountSets( index: { name: NAME } where: { name: { like: "" } journalId: { eq: "822cb59f-ce51-4837-8391-2af3b7a5fc51" } } first: 10 ) { nodes { accountSetId name description } } } ``` **Response** ```json { "data": { "accountSets": { "nodes": [ { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets." }, { "accountSetId": "0b195688-c1e6-4577-b5dd-0075c2feca35", "name": "Settlement", "description": "All settlement accounts." } ] } } } ``` ## accounts Select one or more accounts. Specify the index to use and apply filters to your query. #### Resolves to [`AccountConnection!`](/docs/reference/graphql/types/object#account-connection) #### Arguments --- * ``index`` - [`AccountIndexInput!`](/docs/reference/graphql/types/input#account-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`AccountFilterInput`](/docs/reference/graphql/types/input#account-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetAccounts { accounts( index: { name: CODE } where: { code: { like: "CUST." } } first: 2 ) { nodes { accountId code name description normalBalanceType status } } } ``` **Response** ```json { "data": { "accounts": { "nodes": [ { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "code": "CUST.Alicia", "name": "Alicia", "description": "Alicia's customer wallet.", "normalBalanceType": "DEBIT", "status": "ACTIVE" }, { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "code": "CUST.Bobby", "name": "Bobby", "description": "Bobby's customer wallet.", "normalBalanceType": "DEBIT", "status": "ACTIVE" } ] } } } ``` ## ach ### configuration Read an existing configuration for processing ACH files. #### Resolves to [`AchConfiguration`](/docs/reference/graphql/types/object#ach-configuration) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. _Example:_ ``"fe27128a-b331-4e0e-94f8-9a32443fee36"`` **Request** ```graphql query Configuration { ach { configuration(id: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff") { configId timeZone } } } ``` **Response** ```json { "data": { "ach": { "configuration": { "configId": "1dc71d60-f463-4bb6-b82a-ab42e2f923ff", "timeZone": "America/Los_Angeles" } } } } ``` ### configurations #### Resolves to [`AchConfigurationConnection!`](/docs/reference/graphql/types/object#ach-configuration-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. **Request** ```graphql query Configurations { ach { configurations(first: 100) { nodes { configId timeZone } } } } ``` **Response** ```json { "data": { "ach": { "configurations": { "nodes": [ { "configId": "1dc71d60-f463-4bb6-b82a-ab42e2f923ff", "timeZone": "America/Los_Angeles" } ] } } } } ``` ### file Read the file info for status of file processing. #### Resolves to [`AchFileInfo`](/docs/reference/graphql/types/object#ach-file-info) #### Arguments --- * ``id`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier returned from processFile mutation. _Example:_ ``"549f0093-6476-4625-b38e-2109a0d5d3f8"`` --- * ``fileKey`` - [`String`](/docs/reference/graphql/types/scalar#string) * File key of uploaded ACH file. _Example:_ ``"ppd-credits.ach"`` --- * ``configId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Configuration identifier used in processFile mutation. _Example:_ ``"987908ab-34d4-42f5-9213-c835f96545e1"`` ### files #### Resolves to [`AchFileInfoConnection!`](/docs/reference/graphql/types/object#ach-file-info-connection) #### Arguments --- * ``index`` - [`AchFileInfoIndexInput!`](/docs/reference/graphql/types/input#ach-file-info-index-input) * --- * ``where`` - [`AchFileInfoFilterInput!`](/docs/reference/graphql/types/input#ach-file-info-filter-input) * --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## admin Queries in the `admin` namespace retrieve organization data like users, groups, and tenants. ### groups Get the list of groups, ordered by `name`. #### Resolves to [`GroupConnection!`](/docs/reference/graphql/types/object#group-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetAdminGroups { admin { groups(first: 5) { nodes { name description policy } } } } ``` **Response** ```json { "data": { "admin": { "groups": { "nodes": [ { "name": "Admin", "description": "Default Group", "policy": "[{\"actions\": [\"*\"],\"effect\": \"ALLOW\",\"resources\":[\"*\"]}]" }, { "name": "deny-policy-1", "description": "a group with a deny policy", "policy": "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"]}]" }, { "name": "read-only-policy-1", "description": "a group with a read-only policy", "policy": "[{\"actions\": [\"db:Select\"],\"effect\": \"ALLOW\",\"resources\":[\"*\"]}]" } ] } } } } ``` ### organization Get the current organization. #### Resolves to [`Organization!`](/docs/reference/graphql/types/object#organization) #### Arguments **Request** ```graphql query GetAdminOrganization { admin { organization { id name description } } } ``` **Response** ```json { "data": { "admin": { "organization": { "id": "932343ab-33ee-410d-9663-7ddaec4d5bfa", "name": "test_org", "description": "this is a test organization" } } } } ``` ### restoreStatus Retrieves the status of a restoration. #### Resolves to [`RestoreStatus`](/docs/reference/graphql/types/object#restore-status) #### Arguments --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ### tenants Get the list of tenants, ordered by `accountId`. #### Resolves to [`TenantConnection!`](/docs/reference/graphql/types/object#tenant-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetAdminTenants { admin { tenants(first: 5) { nodes { name organizationId description } } } } ``` **Response** ```json { "data": { "admin": { "tenants": { "nodes": [ { "name": "PreProduction", "organizationId": "932343ab-33ee-410d-9663-7ddaec4d5bfa", "description": "tenant for preprod integrations" }, { "name": "TestTenant", "organizationId": "932343ab-33ee-410d-9663-7ddaec4d5bfa", "description": "this is a test tenant" } ] } } } } ``` ### usage Retrieves the read and write usage units on per day basis. The metrics returned are on the half open interval. returns metris `period >= 2025-06-01T00:00:00.000Z && period < 2025-07-01T00:00:00.000Z` #### Resolves to [`Usage`](/docs/reference/graphql/types/object#usage) #### Arguments --- * ``input`` - [`UsageInput!`](/docs/reference/graphql/types/input#usage-input) * ### users Get the list of human users, ordered by `email`. #### Resolves to [`UserConnection!`](/docs/reference/graphql/types/object#user-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetAdminUsers { admin { users(first: 5) { nodes { email organizationId groupIds } } } } ``` **Response** ```json { "data": { "admin": { "users": { "nodes": [ { "email": "test@twisp.com", "organizationId": "932343ab-33ee-410d-9663-7ddaec4d5bfa", "groupIds": ["4dcef8d5-186b-4db1-aa00-8d5b2eee85f8"] } ] } } } } ``` ## attachedControls List all velocity controls attached the specified Account or Account Set. #### Resolves to [`ResolvedVelocityControlConnection!`](/docs/reference/graphql/types/object#resolved-velocity-control-connection) #### Arguments --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account or Account Set Id to look up attached controls --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``after`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. _Default:_ ``""`` ## auth Queries in the `auth` namespace are used to retrieve clients and their policies. Use the `client` query to retrieve a single client, and `clients` to retrieve a list of clients. ### client Get a single client by the `principal`. #### Resolves to [`Client`](/docs/reference/graphql/types/object#client) #### Arguments --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Principal of the client. **Request** ```graphql query GetAuthClient($authClientGithub: String!) { auth { client(principal: $authClientGithub) { principal name policies { effect actions resources } } } } ``` **Response** ```json { "data": { "auth": { "client": { "principal": "arn:aws:iam::048962233173:user/github.action", "name": "Github", "policies": [ { "effect": "ALLOW", "actions": ["SELECT", "INSERT", "UPDATE", "DELETE"], "resources": ["financial.*"] } ] } } } } ``` **Variables** ```json { "authClientGithub": "arn:aws:iam::048962233173:user/github.action" } ``` ### clients Lists all clients. #### Resolves to [`ClientConnection!`](/docs/reference/graphql/types/object#client-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetAuthClients { auth { clients(first: 10) { nodes { principal name } } } } ``` **Response** ```json { "data": { "auth": { "clients": { "nodes": [ { "principal": "arn:aws:iam::048962233173:user/feed.action", "name": "Read Stream" }, { "principal": "arn:aws:iam::048962233173:user/github.action", "name": "Github" } ] } } } } ``` ## balance Get a balance for an account. #### Resolves to [`Balance`](/docs/reference/graphql/types/object#balance) #### Arguments --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * ID of the journal for the balance. If omitted, the default journal will be used. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the account for the balance. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * Currency of the balance. _Default:_ ``"USD"`` --- * ``calculationId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * If provided, look up based on calculation id and dimensions for calculation. --- * ``dimension`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * If provided, look up based on calculation id and dimensions for calculation. The values in this should be string representation of values. --- * ``effective`` - [`Effective`](/docs/reference/graphql/types/input#effective) * If enabled and provided, look up balances based on effective date. --- * ``materialize`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * If concurrent enabled, compute balance with all visible transactions. --- * ``type`` - [`BalanceType`](/docs/reference/graphql/types/enum#balance-type) * If concurrent enabled, the isolation level of the balance returned. **Request** ```graphql query GetBalance($journalGLId: UUID!, $accountCardSettlementId: UUID!) { balance( accountId: $accountCardSettlementId journalId: $journalGLId currency: "USD" ) { available(layer: SETTLED) { drBalance { units } crBalance { units } normalBalance { formatted(as: { locale: "en-US" }) } } entries(first: 10) { nodes { entryType amount { units currency } direction layer } } } } ``` **Response** ```json { "data": { "balance": { "available": { "drBalance": { "units": "4.53" }, "crBalance": { "units": "0" }, "normalBalance": { "formatted": "-$4.53" } }, "entries": { "nodes": [ { "entryType": "CARD_HOLD_CLEAR_CR", "amount": { "units": "4.53", "currency": "USD" }, "direction": "CREDIT", "layer": "PENDING" }, { "entryType": "CARD_SETTLEMENT_DR", "amount": { "units": "4.53", "currency": "USD" }, "direction": "DEBIT", "layer": "SETTLED" }, { "entryType": "CARD_HOLD_DR", "amount": { "units": "4.53", "currency": "USD" }, "direction": "DEBIT", "layer": "PENDING" } ] } } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` ## balances Select one or more balances. Specify the index to use and apply filters to your query. #### Resolves to [`BalanceConnection!`](/docs/reference/graphql/types/object#balance-connection) #### Arguments --- * ``index`` - [`BalanceIndexInput!`](/docs/reference/graphql/types/input#balance-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`BalanceFilterInput`](/docs/reference/graphql/types/input#balance-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetBalances($journalGLId: String!, $accountCustomerAliciaId: String!) { balances( index: { name: ACCOUNT_ID } where: { accountId: { eq: $accountCustomerAliciaId } journalId: { eq: $journalGLId } } first: 20 ) { nodes { entry { entryType } currency settled { normalBalance { formatted(as: { locale: "en-US" }) } } version } } } ``` **Response** ```json { "data": { "balances": { "nodes": [ { "entry": { "entryType": "FX_BUY_CR" }, "currency": "EUR", "settled": { "normalBalance": { "formatted": "€2.0145" } }, "version": 1 }, { "entry": { "entryType": "FX_SELL_DR" }, "currency": "USD", "settled": { "normalBalance": { "formatted": "$1.63" } }, "version": 6 } ] } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c" } ``` ## bulk ### execution Retrieve single bulk query execution by it's `executionId`. #### Resolves to [`BulkQueryExecution`](/docs/reference/graphql/types/object#bulk-query-execution) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ### executions List bulk query executions. #### Resolves to [`BulkQueryExecutionConnection!`](/docs/reference/graphql/types/object#bulk-query-execution-connection) #### Arguments --- * ``index`` - [`BulkQueryExecutionIndexInput!`](/docs/reference/graphql/types/input#bulk-query-execution-index-input) * Select the index to use. --- * ``where`` - [`BulkQueryExecutionFilterInput`](/docs/reference/graphql/types/input#bulk-query-execution-filter-input) * Filter conditions. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor for pagination. ## calculation Retrieve a calculation by its identifier. #### Resolves to [`Calculation`](/docs/reference/graphql/types/object#calculation) #### Arguments --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. **Request** ```graphql query ReadCalculation { calculation(calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5") { calculationId code description dimensions { alias value } } } ``` **Response** ```json { "data": { "calculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "code": "EFFECTIVE_DATE", "description": "Track balances per EFFECTIVE_DATE in an account.", "dimensions": [ { "alias": "effectiveDate", "value": "context.vars.transaction.effective" } ] } } } ``` ## calculations Retrieve calculations by index. #### Resolves to [`CalculationConnection!`](/docs/reference/graphql/types/object#calculation-connection) #### Arguments --- * ``index`` - [`CalculationIndexInput!`](/docs/reference/graphql/types/input#calculation-index-input) * --- * ``where`` - [`CalculationFilterInput`](/docs/reference/graphql/types/input#calculation-filter-input) * --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. **Request** ```graphql query ListActiveGlobalCalculations { calculations( index: { name: GLOBAL } where: { status: { eq: "ACTIVE" } } first: 1 ) { nodes { calculationId code description dimensions { alias value } } } } ``` **Response** ```json { "data": { "calculations": { "nodes": [ { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "code": "EFFECTIVE_DATE", "description": "Track balances per EFFECTIVE_DATE in an account.", "dimensions": [ { "alias": "effectiveDate", "value": "context.vars.transaction.effective" } ] } ] } } } ``` ## entries Select one or more entries. Specify the index to use and apply filters to your query. #### Resolves to [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) #### Arguments --- * ``index`` - [`EntryIndexInput!`](/docs/reference/graphql/types/input#entry-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`EntryFilterInput`](/docs/reference/graphql/types/input#entry-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int`](/docs/reference/graphql/types/scalar#int) * Return the first n-edges of a search. Use `after` to paginate forward. --- * ``last`` - [`Int`](/docs/reference/graphql/types/scalar#int) * Return previous n-edges of a search relative to the `before` cursor. **NOTE:** only available on search indexes. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor to paginate forward through a search result. When no cursor is provided, the query uses the default starting cursor. --- * ``before`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor to paginate backwards through a search result. **NOTE:** Only available on search indexes. **Request** ```graphql query GetEntries { entries( index: { name: TRANSACTION_ID } where: { transactionId: { eq: "114a8b1e-d00b-4e13-ab27-ec3472622c0a" } } first: 10 ) { nodes { entryType account { name } direction amount { units currency } } } } ``` **Response** ```json { "data": { "entries": { "nodes": [ { "entryType": "CARD_HOLD_CR", "account": { "name": "Alicia" }, "direction": "CREDIT", "amount": { "units": "4.53", "currency": "USD" } }, { "entryType": "CARD_HOLD_DR", "account": { "name": "Card Settlement" }, "direction": "DEBIT", "amount": { "units": "4.53", "currency": "USD" } } ] } } } ``` ## entry Get a single entry by its `entryId`. #### Resolves to [`Entry`](/docs/reference/graphql/types/object#entry) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. ## events Mutations in the `events` namespace are used to manage event subscriptions, such as webhooks. ### endpoint Get a single endpoint by it's `endpointId` #### Resolves to [`Endpoint`](/docs/reference/graphql/types/object#endpoint) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. **Request** ```graphql query GetEndpoint { events { endpoint(id: "345940ed-2726-4b20-88aa-820857ac0e68") { endpointId status endpointType url subscription description } } } ``` **Response** ```json { "data": { "events": { "endpoint": { "endpointId": "345940ed-2726-4b20-88aa-820857ac0e68", "status": "ENABLED", "endpointType": "WEBHOOK", "url": "https://webhook.site/twisp-webhook-test", "subscription": ["balance*", "account.*"], "description": "subscribe to balance and account events" } } } } ``` ### endpoints Select one or more endpoints. Specify the index to use and apply filters to your query. #### Resolves to [`EndpointConnection!`](/docs/reference/graphql/types/object#endpoint-connection) #### Arguments --- * ``index`` - [`EndpointIndexInput!`](/docs/reference/graphql/types/input#endpoint-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`EndpointFilterInput!`](/docs/reference/graphql/types/input#endpoint-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. ## files ### list #### Resolves to [`[String]`]($gql:scalar:String) #### Arguments --- * ``keyPrefix`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## iso8583 ### config Read an existing ISO8583 configuration. #### Resolves to [`ISO8583Config`](/docs/reference/graphql/types/object#iso8583config) #### Arguments --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. _Example:_ ``"670504f6-0515-11f0-a441-069b540ea27c"`` **Request** ```graphql query GetISO8583Config { iso8583 { config(configId: "670504f6-0515-11f0-a441-069b540ea27c") { id configId journalId settlementAccountId timeZone description processor version } } } ``` **Response** ```json { "data": { "iso8583": { "config": { "id": "iso8583:config:670504f6-0515-11f0-a441-069b540ea27c", "configId": "670504f6-0515-11f0-a441-069b540ea27c", "journalId": "8b65c8dc-0515-11f0-a441-069b540ea27c", "settlementAccountId": "779886bc-0515-11f0-aa78-069b540ea27c", "timeZone": "America/Chicago", "description": "ISO8583 Test Config 1", "processor": "I2C", "version": 1 } } } } ``` ### configs List all ISO8583 configurations. #### Resolves to [`ISO8583ConfigConnection!`](/docs/reference/graphql/types/object#iso8583config-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * Sort order for results. **Request** ```graphql query ListISO8583Configs { iso8583 { configs(first: 10) { pageInfo { hasNextPage hasPreviousPage } edges { node { configId journalId settlementAccountId timeZone description processor } } } } } ``` **Response** ```json { "data": { "iso8583": { "configs": { "pageInfo": { "hasNextPage": false, "hasPreviousPage": false }, "edges": [ { "node": { "configId": "670504f6-0515-11f0-a441-069b540ea27c", "journalId": "8b65c8dc-0515-11f0-a441-069b540ea27c", "settlementAccountId": "779886bc-0515-11f0-aa78-069b540ea27c", "timeZone": "America/Chicago", "description": "ISO8583 Test Config 1", "processor": "I2C" } }, { "node": { "configId": "7e0d3fa2-0515-11f0-a6b2-069b540ea27c", "journalId": "8b65c8dc-0515-11f0-a441-069b540ea27c", "settlementAccountId": "779886bc-0515-11f0-aa78-069b540ea27c", "timeZone": "America/New_York", "description": "ISO8583 Test Config 2", "processor": "I2C" } } ] } } } } ``` ## journal Get a single journal by its `journalId`. If `journalId` is omitted, return the default journal. #### Resolves to [`Journal`](/docs/reference/graphql/types/object#journal) #### Arguments --- * ``id`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql query GetJournal($journalGLId: UUID!) { journal(id: $journalGLId) { name description status version } } ``` **Response** ```json { "data": { "journal": { "name": "GL", "description": "General Ledger", "status": "ACTIVE", "version": 1 } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## journals Select one or more journals. Specify the index to use and apply filters to your query. #### Resolves to [`JournalConnection!`](/docs/reference/graphql/types/object#journal-connection) #### Arguments --- * ``index`` - [`JournalIndexInput!`](/docs/reference/graphql/types/input#journal-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`JournalFilterInput`](/docs/reference/graphql/types/input#journal-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetJournals { journals( index: { name: STATUS } where: { status: { eq: "ACTIVE" } } first: 10 ) { nodes { journalId name description status } } } ``` **Response** ```json { "data": { "journals": { "nodes": [ { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "GL", "description": "General Ledger", "status": "ACTIVE" }, { "journalId": "432d2b1b-c647-4b0a-aa78-0945241f8e6d", "name": "SL", "description": "Securities Ledger", "status": "ACTIVE" } ] } } } ``` ## kv Read a single key/value record by its natural key. Limits: - `namespace`: max 512 UTF-8 bytes - `key`: max 512 UTF-8 bytes #### Resolves to [`KVValue`](/docs/reference/graphql/types/object#kvvalue) #### Arguments --- * ``namespace`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## kvs List key/value records through either the built-in namespace index or a custom index. Notes: - `index: { name: Namespace }` requires `where.namespace.eq` - `index: { name: Custom }` requires `where.custom.index` plus the custom index partition filter(s) #### Resolves to [`KVConnection!`](/docs/reference/graphql/types/object#kvconnection) #### Arguments --- * ``index`` - [`KVIndexInput!`](/docs/reference/graphql/types/input#kvindex-input) * --- * ``where`` - [`KVFilterInput`](/docs/reference/graphql/types/input#kvfilter-input) * --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## node #### Resolves to [`Node`](/docs/reference/graphql/types/interface#node) #### Arguments --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. ## schema Queries in the `schema` namespace are used to retrieve information about custom indexes, aggregates, and historical indexes. ### index Get a single index by `name`. #### Resolves to [`Index`](/docs/reference/graphql/types/object#index) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``on`` - [`IndexOnEnum!`](/docs/reference/graphql/types/enum#index-on-enum) * The type of record this index applies to. --- * ``viewName`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name of the target view. Required when `on: View`; ignored otherwise. **Request** ```graphql query GetSchemaIndex($indexName: String!) { schema { index(name: $indexName, on: Account) { name on unique range { alias value sort } partition { alias value } constraints } } } ``` **Response** ```json { "data": { "schema": { "index": { "name": "Account.metadata.type", "on": "Account", "unique": false, "range": [ { "alias": "type", "value": "string(document.metadata.type)", "sort": "ASC" } ], "partition": [ { "alias": "type", "value": "string(document.metadata.type)" } ], "constraints": { "hasCategory": "has(document.metadata.type)" } } } } } ``` **Variables** ```json { "indexName": "Account.metadata.type" } ``` ### indexes List all indexes, ordered by `name`. #### Resolves to [`IndexConnection!`](/docs/reference/graphql/types/object#index-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetSchemaIndexes { schema { indexes(first: 10) { nodes { name range { alias value sort } partition { alias value } on constraints unique } } } } ``` **Response** ```json { "data": { "schema": { "indexes": { "nodes": [ { "name": "Account.metadata.type", "range": [ { "alias": "type", "value": "string(document.metadata.type)", "sort": "ASC" } ], "partition": [ { "alias": "type", "value": "string(document.metadata.type)" } ], "on": "Account", "constraints": { "hasCategory": "has(document.metadata.type)" }, "unique": false }, { "name": "AccountSet.metadata.type", "range": [ { "alias": "type", "value": "string(document.metadata.type)", "sort": "ASC" } ], "partition": [ { "alias": "type", "value": "string(document.metadata.type)" } ], "on": "AccountSet", "constraints": { "hasCategory": "has(document.metadata.type)" }, "unique": false }, { "name": "Transaction.metadata.category", "range": [ { "alias": "category", "value": "string(document.metadata.category)", "sort": "ASC" } ], "partition": [ { "alias": "category", "value": "string(document.metadata.category)" } ], "on": "Transaction", "constraints": { "hasCategory": "has(document.metadata.category)" }, "unique": false } ] } } } } ``` ### view Get a single view by `name`. #### Resolves to [`View`](/docs/reference/graphql/types/object#view) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this view. Typically human readable. ### views List all views, ordered by `name`. #### Resolves to [`ViewConnection!`](/docs/reference/graphql/types/object#view-connection) #### Arguments --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. ## tranCode Get a single tran code by its `tranCodeId`. #### Resolves to [`TranCode`](/docs/reference/graphql/types/object#tran-code) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql query GetTranCode($tcCardHoldId: UUID!) { tranCode(id: $tcCardHoldId) { code description params { name type description } transaction { journalId effective correlationId description } entries { accountId layer direction units currency } status metadata } } ``` **Response** ```json { "data": { "tranCode": { "code": "CARD_HOLD", "description": "Place an authorization hold on an account for the amount specified.", "params": [ { "name": "account", "type": "UUID", "description": "The account to place the hold on." }, { "name": "amount", "type": "DECIMAL", "description": "The amount of the hold." }, { "name": "currency", "type": "STRING", "description": "Currency used for transaction." }, { "name": "isDebit", "type": "BOOLEAN", "description": "If true (default), debit the account specified. If false, credit the account." }, { "name": "correlation", "type": "STRING", "description": "Correlation identifier to group transactions following on from this hold." }, { "name": "effective", "type": "DATE", "description": "Effective date of the transaction." } ], "transaction": { "journalId": "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')", "effective": "params.effective", "correlationId": "params.correlation", "description": "'Card authorization hold placed for ' + string(params.amount) + ' ' + params.currency" }, "entries": [ { "accountId": "params.account", "layer": "PENDING", "direction": "params.isDebit ? DEBIT : CREDIT", "units": "params.amount", "currency": "params.currency" }, { "accountId": "uuid('a9c8dde6-c0e5-407c-9d99-029c523f7ea8')", "layer": "PENDING", "direction": "params.isDebit ? CREDIT : DEBIT", "units": "params.amount", "currency": "params.currency" } ], "status": "ACTIVE", "metadata": { "category": "Card" } } } } ``` **Variables** ```json { "tcCardHoldId": "8f67fb0b-795a-47b3-9b03-40351e8ac584" } ``` ## tranCodes Select one or more tran codes. Specify the index to use and apply filters to your query. #### Resolves to [`TranCodeConnection!`](/docs/reference/graphql/types/object#tran-code-connection) #### Arguments --- * ``index`` - [`TranCodeIndexInput!`](/docs/reference/graphql/types/input#tran-code-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`TranCodeFilterInput`](/docs/reference/graphql/types/input#tran-code-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetTranCodes { tranCodes( index: { name: STATUS } where: { status: { eq: "ACTIVE" } } first: 10 ) { nodes { code metadata } } } ``` **Response** ```json { "data": { "tranCodes": { "nodes": [ { "code": "ACH_CREDIT", "metadata": { "category": "ACH" } }, { "code": "ACH_DEBIT", "metadata": { "category": "ACH" } }, { "code": "BOOK_TRANSFER", "metadata": { "category": "Internal" } }, { "code": "CARD_HOLD", "metadata": { "category": "Card" } }, { "code": "CARD_HOLD_CANCEL", "metadata": { "category": "Card" } }, { "code": "CARD_HOLD_EXPIRE", "metadata": { "category": "Card" } }, { "code": "CARD_HOLD_MODIFICATION", "metadata": { "category": "Card" } }, { "code": "CARD_TX_CLEARING", "metadata": { "category": "Card" } }, { "code": "EXCHANGE_CURRENCY", "metadata": { "category": "FX" } } ] } } } ``` ## transaction Get a single transaction by its `transactionId`. #### Resolves to [`Transaction`](/docs/reference/graphql/types/object#transaction) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier. **Request** ```graphql query GetTransaction { transaction(id: "434696a7-56e5-4e14-a97a-884820690a22") { description effective tranCode { code } journal { name } metadata entries(first: 10) { nodes { sequence entryType layer direction account { name } amount { formatted(as: { locale: "en-US" }) } } } } } ``` **Response** ```json { "data": { "transaction": { "description": "Exchange 2.37 USD for EUR at a 1:0.85 exchange rate.", "effective": "2022-09-11", "tranCode": { "code": "EXCHANGE_CURRENCY" }, "journal": { "name": "GL" }, "metadata": { "buy": "EUR", "rate": "0.85", "sell": "USD" }, "entries": { "nodes": [ { "sequence": 0, "entryType": "FX_SELL_DR", "layer": "SETTLED", "direction": "CREDIT", "account": { "name": "Alicia" }, "amount": { "formatted": "$2.37" } }, { "sequence": 1, "entryType": "FX_SELL_CR", "layer": "SETTLED", "direction": "DEBIT", "account": { "name": "Foreign Exchange Broker" }, "amount": { "formatted": "$2.37" } }, { "sequence": 2, "entryType": "FX_BUY_DR", "layer": "SETTLED", "direction": "CREDIT", "account": { "name": "Foreign Exchange Broker" }, "amount": { "formatted": "€2.0145" } }, { "sequence": 3, "entryType": "FX_BUY_CR", "layer": "SETTLED", "direction": "DEBIT", "account": { "name": "Alicia" }, "amount": { "formatted": "€2.0145" } } ] } } } } ``` ## transactions Select one or more transactions. Specify the index to use and apply filters to your query. #### Resolves to [`TransactionConnection!`](/docs/reference/graphql/types/object#transaction-connection) #### Arguments --- * ``index`` - [`TransactionIndexInput!`](/docs/reference/graphql/types/input#transaction-index-input) * Select from a list of pre-defined indexes. For optimal performance, choose specific indexes on unique fields over more general ones. --- * ``where`` - [`TransactionFilterInput`](/docs/reference/graphql/types/input#transaction-filter-input) * Filter the query according to specified conditions. Depending on the index chosen, different filters will be allowed. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. **Request** ```graphql query GetTransactions($journalGLId: String!) { transactions( index: { name: CORRELATION_ID } where: { correlationId: { eq: "4f2ac9a7-5e83-458b-a194-c8ac8d8fdcd5" } journalId: { eq: $journalGLId } } first: 10 ) { nodes { transactionId description effective tranCode { code } } } } ``` **Response** ```json { "data": { "transactions": { "nodes": [ { "transactionId": "114a8b1e-d00b-4e13-ab27-ec3472622c0a", "description": "Card authorization hold placed for 4.53 USD", "effective": "2022-09-10", "tranCode": { "code": "CARD_HOLD" } }, { "transactionId": "d94033a4-f438-4fcc-9c3b-c7adf3fd7b76", "description": "Card TX for 4.53 USD settled.", "effective": "2022-09-12", "tranCode": { "code": "CARD_TX_CLEARING" } } ] } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## velocity Search for the velocity balance for an Account or Account Set. #### Resolves to [`[VelocityBalance]`]($gql:object:VelocityBalance) #### Arguments --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account or Account Set to search for velocity. --- * ``window`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * The window of the balance to look up. If `velocityLimitId` not provided, will look up all velocity limits that support the defined window. _Example:_ ``{year: "2022", month:"9"}`` --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * The currency for the velocity to look up. _Default:_ ``"USD"`` --- * ``velocityLimitId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * If provided, retrieve this specific velocity limit. --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * If provided, retrieve from specified journal. Otherwise will use default. ## velocityControl #### Resolves to [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ## velocityControls #### Resolves to [`VelocityControlConnection`](/docs/reference/graphql/types/object#velocity-control-connection) #### Arguments --- * ``index`` - [`VelocityControlIndexInput!`](/docs/reference/graphql/types/input#velocity-control-index-input) * --- * ``where`` - [`VelocityControlFilterInput`](/docs/reference/graphql/types/input#velocity-control-filter-input) * --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. _Default:_ ``100`` --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## velocityLimit #### Resolves to [`VelocityLimit`](/docs/reference/graphql/types/object#velocity-limit) #### Arguments --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ## velocityLimits #### Resolves to [`VelocityLimitConnection`](/docs/reference/graphql/types/object#velocity-limit-connection) #### Arguments --- * ``index`` - [`VelocityLimitIndexInput!`](/docs/reference/graphql/types/input#velocity-limit-index-input) * --- * ``where`` - [`VelocityLimitFilterInput`](/docs/reference/graphql/types/input#velocity-limit-filter-input) * --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. _Default:_ ``100`` --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## view Get entries from a specific view by name. #### Resolves to [`ViewRecordConnection!`](/docs/reference/graphql/types/object#view-record-connection) #### Arguments --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name of the view to query. --- * ``where`` - [`ViewFilter`](/docs/reference/graphql/types/input#view-filter) * Filters to apply based on the view's dimensions. --- * ``first`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of nodes to return on the connection. --- * ``after`` - [`String`](/docs/reference/graphql/types/scalar#string) * Cursor indicating the start point to retrieve nodes. When no cursor is provided, the query uses the default starting cursor. ## warehouse Queries in the `warehouse` namespace are used to perform reads against the twisp data warehouse ### describeStatement #### Resolves to [`DescribeStatementOutput`](/docs/reference/graphql/types/object#describe-statement-output) #### Arguments --- * ``input`` - [`DescribeStatementInput!`](/docs/reference/graphql/types/input#describe-statement-input) * ### describeTable #### Resolves to [`DescribeTableOutput`](/docs/reference/graphql/types/object#describe-table-output) #### Arguments --- * ``input`` - [`DescribeTableInput!`](/docs/reference/graphql/types/input#describe-table-input) * ### export #### Resolves to [`Export`](/docs/reference/graphql/types/object#export) #### Arguments --- * ``id`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ### getStatementResult #### Resolves to [`GetStatementResultOutput`](/docs/reference/graphql/types/object#get-statement-result-output) #### Arguments --- * ``input`` - [`GetStatementResultInput!`](/docs/reference/graphql/types/input#get-statement-result-input) * ### listDatabases #### Resolves to [`ListDatabasesOutput`](/docs/reference/graphql/types/object#list-databases-output) #### Arguments --- * ``input`` - [`ListDatabasesInput!`](/docs/reference/graphql/types/input#list-databases-input) * ### listSchemas #### Resolves to [`ListSchemasOutput`](/docs/reference/graphql/types/object#list-schemas-output) #### Arguments --- * ``input`` - [`ListSchemasInput!`](/docs/reference/graphql/types/input#list-schemas-input) * ### listStatements #### Resolves to [`ListStatementsOutput`](/docs/reference/graphql/types/object#list-statements-output) #### Arguments --- * ``input`` - [`ListStatementsInput!`](/docs/reference/graphql/types/input#list-statements-input) * ### listTables #### Resolves to [`ListTablesOutput`](/docs/reference/graphql/types/object#list-tables-output) #### Arguments --- * ``input`` - [`ListTablesInput!`](/docs/reference/graphql/types/input#list-tables-input) * **Request** ```graphql query listTables { warehouse { listTables( input: { maxResults: 5, database: "twisp", schemaPattern: "public" } ) { tables { name } } } } ``` **Response** ```json { "data": { "warehouse": { "listTables": { "tables": [ { "name": "account_history" }, { "name": "accountcontext_history" }, { "name": "accountset_history" }, { "name": "accountsetmember_history" }, { "name": "backfilljob_history" } ] } } } } ``` **Variables** ```json {} ``` ## workflow ### execution Return the execution by execution id. #### Resolves to [`WorkflowExecution`](/docs/reference/graphql/types/object#workflow-execution) #### Arguments --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- # Enum Types Enum types define a set of predefined values that a field can take. Source: https://www.twisp.com/docs/reference/graphql/types/enum ## AccountIndex Indexes for querying Accounts. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``ACCOUNT_ID`` * Index by `accountId` field. Must supply an `accountId: { eq: }` filter to the `where` object. --- * ``NAME`` * Eventually consistent index by `name` field. Use to apply query filters on the value of `name`. --- * ``CODE`` * Eventually consistent index by `code` field. When an `eq` filter is supplied, a strongly consistent index is used. Use to apply query filters on the value of `code`. --- * ``STATUS`` * Eventually consistent index by `status` field. Use to apply query filters on the value of `status`. --- * ``EXTERNAL_ID`` * Index by `externalId` field. Must supply an `externalId: { eq: }` filter to the `where` object. --- * ``CUSTOM`` * Use a custom-defined index. Must supply a `custom` filter to the `where` object. --- * ``SEARCH`` * Use eventually consisent search. Must supply a `search` filter to the `where` object. --- * ``CODE_UNIQUE`` * Index by `code` field. Must supply an `codeUnique: { eq: }` filter to the `where` object. ## AccountSetIndex Indexes for querying AccountSets. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``ACCOUNT_SET_ID`` * Index by `accountSetId` field. Must supply an `accountSetId: { eq: }` filter to the `where` object. --- * ``NAME`` * Eventually consistent index by `name` field. Use to apply query filters on the value of `name`. --- * ``CODE`` * Eventually consistent index by `code` field. When an `eq` filter is supplied, a strongly consistent index is used. Use to apply query filters on the value of `code`. --- * ``CUSTOM`` * Use a custom-defined index. Must supply a `custom` filter to the `where` object. --- * ``SEARCH`` * Use eventually consisent search. Must supply a `search` filter to the `where` object. ## AccountSetMemberType Account set members can be of type Account or AccountSet. #### Values --- * ``ACCOUNT`` * --- * ``ACCOUNT_SET`` * ## AccountStatus Account status determines whether the account is in active use or closed (locked). By default, all accounts are `ACTIVE`. When account is `LOCKED`, it cannot be changed and any attempt to write a ledger entry to this account will raise an error. #### Values --- * ``ACTIVE`` * ACTIVE = Account is open for posting. --- * ``LOCKED`` * LOCKED = Account is locked and will block posting. --- * ``INACTIVE`` * INACTIVE = Account is open for posting but will be LOCKED soon. ## AchFileInfoIndex #### Values --- * ``PROCESSING_STATUS`` * Index by `processingStatus` field. Must supply both `processingStatus:{eq: }` and `configId:{eq: }` to the `where` object. ## AchFileProcessingStatus #### Values --- * ``UNKNOWN`` * File Processing state is unknown. --- * ``NEW`` * Newly created file processing state. --- * ``VALIDATING`` * File is being validated. --- * ``PARTITIONING`` * File is being partitioned for processing. --- * ``UPLOADED`` * File has been uploaded and will begin processing shortly. --- * ``PROCESSING`` * File is being processed and create webhooks are being actively sent for this file. --- * ``PROCESSED`` * All create webhooks have been sent for this file and entries are awaiting settlement. --- * ``ERROR`` * The system encountered an unrecoverable error while processing the file. See `processingDetail` for more information. --- * ``INVALID`` * The uploaded file failed validation. See `processingDetail` for more information. --- * ``ABORTED`` * The processing of this file was aborted. See `processingDetail` for more information. --- * ``COMPLETED`` * All entries from this file process have been either settled or queued for a return file. ## AchFileType #### Values --- * ``RDFI`` * --- * ``RDFI_RETURN`` * --- * ``RDFI_NOC`` * --- * ``ODFI`` * --- * ``ODFI_PULL_ONLY`` * --- * ``ODFI_PUSH_ONLY`` * --- * ``ODFI_RETURN`` * --- * ``ODFI_PREPROCESS_RETURN`` * --- * ``ODFI_PROCESSED`` * ## BalanceIndex Indexes for querying Balances. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``ACCOUNT_ID`` * Index by `accountId` field. Must supply an `accountId: { eq: }` filter to the `where` object. --- * ``CALCULATION`` * Index by `calculationId` and `dimension` fields in addition to account. --- * ``CUSTOM`` * Use a custom-defined index. Must supply a `custom` filter to the `where` object. --- * ``SEARCH`` * Use eventually consisent search. Must supply a `search` filter to the `where` object. ## BalanceLimitType #### Values --- * ``AVAILABLE`` * Limit based on available balance at a particular layer. ## BalanceType All operations in Twisp are **strongly consistent** and transactionally isolated. Both concurrent and non-concurrent enabled accounts provide the same strong consistency guarantee on balances, but concurrent enabled accounts can adjust their balance isolation levels and latency characteristics according to different user needs. #### Values --- * ``FINAL`` * Return the latest calculated balance record for the account. This balance may be slightly stale but offers the lowest latency. --- * ``PREPARED`` * Return the latest calculated balance record plus any committed, but not yet FINAL, entries. This balance is transactionally consistent for the current snapshot and guaranteed to eventually match a FINAL balance record. --- * ``PROVISIONAL`` * Return the latest calculated balance record plus any committed, but not yet FINAL, entries along with all in-flight uncommitted entries. This balance is provisional since uncommitted entries are not guaranteed to commit. ## BulkQueryExecutionIndex Indexes for querying bulk executions. #### Values --- * ``STATUS`` * Eventually consistent index by `status` field, sorted by `created` timestamp. ## BulkQueryExecutionStatus #### Values --- * ``CREATED`` * Execution is created but not yet started. --- * ``IN_PROGRESS`` * Execution is in progress. --- * ``SUMMARIZING`` * Execution is complete and results are being summarized. --- * ``COMPLETE`` * Execution is complete and results are available to download. --- * ``ERROR`` * There was an error that prevented execution. --- * ``CANCELLED`` * Execution was cancelled by the user. ## CalculationBackfillStatus #### Values --- * ``COMPLETE`` * Calculation is completely backfilled or was created before backfill support. --- * ``IN_PROGRESS`` * Backfill is in progress. --- * ``ERROR`` * There was an error in the backfill. --- * ``DETACHED`` * The calculation was detached. ## CalculationIndex #### Values --- * ``CALCULATION_ID`` * Index by `calculationId` field. Must supply `calculationId: { eq: }` filter to the `where` object. --- * ``CODE`` * Index by `code` field. Must supply `code: { eq: }` filter to the `where` object. --- * ``GLOBAL`` * Global calculations by `status` field. Must supply `status: { eq: }` filter to the `where` object. --- * ``LOCAL`` * Local calculations by `status` field. Must supply `status: { eq: }` filter to the `where` object. ## CalculationScope #### Values --- * ``GLOBAL`` * The calculation is computed on all accounts and sets in the journal. --- * ``LOCAL`` * The calculation is computed only on accounts and sets where it is attached. ## CalculationStatus #### Values --- * ``ACTIVE`` * Calculation is actively in use. --- * ``LOCKED`` * Calculation has been deleted and no longer in use. ## CelType #### Values --- * ``JSON`` * A json object --- * ``Timestamp`` * RFC-3339 formatted timestamp. --- * ``Duration`` * Golang formatted duration string. --- * ``Double`` * Signed 64 bit floating point. --- * ``Decimal`` * 128-Bit fixed precision decimal. --- * ``String`` * UTF-8 encoded string. --- * ``Bool`` * Boolean --- * ``Int`` * Signed 64-bit integer --- * ``UInt`` * Unsigned 64-bit integer --- * ``Bytes`` * Base64 encoded bytestring. --- * ``UUID`` * UUID supporting v1-8 ## CurrencyDisplay Defines how to render the currency indicator. #### Values --- * ``SYMBOL`` * Show the symbol for the currency. @example('$1.23') @example('€1.23') --- * ``CODE`` * Show the currency code. @example('USD') @example('EUR') --- * ``NONE`` * Don't show the code or symbol for the currency. ## DebitOrCredit Debit or credit? Sometimes these are abbreviated to DR and CR. #### Values --- * ``DEBIT`` * --- * ``CREDIT`` * ## EffectiveGranularity #### Values --- * ``YEAR`` * Year-level effective calculation (YYYY) --- * ``MONTH`` * Month-level effective calculation (YYYY_MM) --- * ``DAY`` * Day-level effective calculation (YYYY_MM_DD) ## EndpointIndex #### Values --- * ``ENDPOINT_ID`` * Index by `endpointId` field. Must supply an `endpointId: { eq: }` filter to the `where` object. --- * ``STATUS`` * Index by `status` field. Must supply an `status: { eq: }` filter to the `where` object. ## EndpointStatus #### Values --- * ``ENABLED`` * --- * ``DISABLED`` * ## EndpointType #### Values --- * ``WEBHOOK`` * --- * ``ACH_PROCESSOR`` * ## EntryIndex Indexes for querying Entries. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``ENTRY_ID`` * Index by `entryId` field. Must supply an `entryId: { eq: }` filter to the `where` object. --- * ``TRANSACTION_ID`` * Index by `transactionId` field. Must supply an `transactionId: { eq: }` filter to the `where` object. --- * ``CUSTOM`` * Use a custom-defined index. Must supply a `custom` filter to the `where` object. --- * ``SEARCH`` * Use eventually consisent search. Must supply a `search` filter to the `where` object. ## ExcludeTableEnum #### Values --- * ``Account`` * --- * ``AccountContext`` * --- * ``AccountSet`` * --- * ``AccountSetMember`` * --- * ``AttachedCalculation`` * --- * ``Balance`` * --- * ``BulkExecution`` * --- * ``Calculation`` * --- * ``Endpoint`` * --- * ``Entry`` * --- * ``Journal`` * --- * ``Transaction`` * --- * ``TransactionException`` * --- * ``TranCode`` * --- * ``VelocityControl`` * --- * ``VelocityControlAccount`` * --- * ``VelocityLimit`` * --- * ``WorkflowExecution`` * ## ExportCompression #### Values --- * ``NONE`` * --- * ``GZIP`` * --- * ``BZIP2`` * --- * ``ZSTD`` * ## ExportEntity #### Values --- * ``Balance`` * --- * ``Account`` * --- * ``AccountSet`` * --- * ``Transaction`` * --- * ``Entry`` * --- * ``AccountSetMember`` * --- * ``WorkflowExecution`` * ## ExportFormat #### Values --- * ``PARQUET`` * --- * ``JSON`` * --- * ``CSV`` * ## ExportVersion #### Values --- * ``LATEST`` * --- * ``HISTORICAL`` * ## FileRecordType #### Values --- * ``FILE`` * --- * ``BATCH`` * --- * ``IAT_BATCH`` * --- * ``ENTRY_DETAIL`` * --- * ``ADV_ENTRY_DETAIL`` * --- * ``IAT_ENTRY_DETAIL`` * ## ISO8583ProcessorType #### Values --- * ``UNSPECIFIED`` * --- * ``VISA_DIRECT`` * --- * ``GALILEO`` * --- * ``MARQETA`` * --- * ``LITHIC`` * --- * ``HIGHNOTE`` * --- * ``Q2`` * --- * ``FIRST_DATA`` * --- * ``I2C`` * ## IndexDataType #### Values --- * ``INT`` * --- * ``UINT`` * --- * ``DOUBLE`` * --- * ``BOOL`` * --- * ``STRING`` * --- * ``BYTES`` * --- * ``DURATION`` * --- * ``TIMESTAMP`` * --- * ``UUID`` * --- * ``DATE`` * --- * ``MONEY`` * --- * ``DECIMAL`` * ## IndexOnEnum Record types which support custom indexes. #### Values --- * ``Account`` * --- * ``AccountSet`` * --- * ``Balance`` * --- * ``Transaction`` * --- * ``TranCode`` * --- * ``Entry`` * --- * ``KV`` * --- * ``View`` * Target a user-defined view. Requires `viewName` on createIndex / deleteIndex. ## IndexStatusEnum #### Values --- * ``CREATING`` * Index is creating and backfilling. --- * ``ACTIVE`` * Index is active ## JobType #### Values --- * ``SETTLE_WORKFLOW`` * ## JournalIndex Indexes for querying Journals. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``JOURNAL_ID`` * Index by `JOURNAL_ID` field. Must supply a `journalId: { eq: }` filter to the `where` object. --- * ``NAME`` * Index by `name` field. Use to apply query filters on the value of `name`. --- * ``STATUS`` * Index by `status` field. Use to apply query filters on the value of `status`. --- * ``CODE`` * Index by `code` field. Use to apply query filters on the value of `code`. ## KVIndex #### Values --- * ``Namespace`` * Query the built-in namespace index. --- * ``Custom`` * Query a user-defined custom index created on `KV`. ## Layer The ledger can apply a entries to one of three layers: SETTLED, PENDING, and ENCUMBRANCE. The SETTLED layer is what is actually fully settled. The PENDING layer is what's settled but also includes holds and pending charges. This can be used to verify the account will have enough funds after the holds and pending transactions have cleared. The ENCUMBRANCE layer allows us to add future transactions that are scheduled and also goals or budgeting tools to set money aside in the account. #### Values --- * ``SETTLED`` * --- * ``PENDING`` * --- * ``ENCUMBRANCE`` * ## OpenToBuyType #### Values --- * ``AVAILABLE_ENCUMBRANCE`` * Available encumbrance balance in the normality of the account. --- * ``AVAILABLE_PENDING`` * Available pending balance in the normality of the account. --- * ``AVAILABLE_SETTLED`` * Available settled balance in the normality of the account. --- * ``VELOCITY_BALANCE`` * A default windowed attached velocity control. ## OpensearchSchemaBinaryMappingType #### Values --- * ``BINARY`` * ## OpensearchSchemaBooleanMappingType #### Values --- * ``BOOLEAN`` * ## OpensearchSchemaDateMappingType #### Values --- * ``DATE`` * --- * ``DATE_NANOS`` * ## OpensearchSchemaNumericMappingType #### Values --- * ``BYTE`` * --- * ``DOUBLE`` * --- * ``FLOAT`` * --- * ``HALF_FLOAT`` * --- * ``INTEGER`` * --- * ``LONG`` * --- * ``UNSIGNED_LONG`` * --- * ``SHORT`` * ## OpensearchSchemaObjectMappingType #### Values --- * ``OBJECT`` * ## OpensearchSchemaStringMappingType #### Values --- * ``KEYWORD`` * --- * ``TEXT`` * --- * ``MATCH_ONLY_TEXT`` * --- * ``TOKEN_COUNT`` * --- * ``WILDCARD`` * ## ParamDataType Data type of a parameter. #### Values --- * ``STRING`` * --- * ``INTEGER`` * --- * ``DECIMAL`` * --- * ``BOOLEAN`` * --- * ``UUID`` * --- * ``DATE`` * --- * ``TIMESTAMP`` * --- * ``JSON`` * ## PolicyAction #### Values --- * ``SELECT`` * --- * ``INSERT`` * --- * ``UPDATE`` * --- * ``DELETE`` * ## PolicyEffect #### Values --- * ``ALLOW`` * --- * ``DENY`` * ## RestoreStatusEnum #### Values --- * ``EXPORTING`` * --- * ``IMPORTING`` * --- * ``COMPLETE`` * --- * ``ERROR`` * ## RoundingMode Defines the rounding behavior when formatting units. #### Values --- * ``HALF_DOWN`` * Rounds up if the next digit is > 5, otherwise rounds down. --- * ``HALF_UP`` * Rounds up if the next digit is >= 5, otherwise rounds down. --- * ``DOWN`` * Rounds towards 0, truncating extra digits. --- * ``UP`` * Rounds away from 0. ## SQLField_Type #### Values --- * ``IS_NULL`` * --- * ``BYTES`` * --- * ``BOOL`` * --- * ``DOUBLE`` * --- * ``INT`` * --- * ``STRING`` * ## SortOrder `ASC` (ascending) or `DESC` (descending). #### Values --- * ``ASC`` * --- * ``DESC`` * ## SqlStatementStatus #### Values --- * ``ABORTED`` * --- * ``ALL`` * --- * ``FAILED`` * --- * ``FINISHED`` * --- * ``PICKED`` * --- * ``STARTED`` * --- * ``SUBMITTED`` * ## Status Record status. All records are `ACTIVE` by default. To avoid rewriting accounting history, most records are not deleted but simply marked `LOCKED`, indicating that they should not be used. #### Values --- * ``ACTIVE`` * --- * ``LOCKED`` * --- * ``INACTIVE`` * ## TranCodeIndex Indexes for querying TranCodes. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``TRAN_CODE_ID`` * Index by `tranCodeId` field. Must supply a `tranCodeId: { eq: }` filter to the `where` object. --- * ``CODE`` * Index by `code` field. Use to apply query filters on the value of `code`. --- * ``STATUS`` * Index by `status` field. Use to apply query filters on the value of `status`. --- * ``CUSTOM`` * Use a custom-defined index. Must supply a `custom` filter to the `where` object. --- * ``SEARCH`` * Use eventually consisent search. Must supply a `search` filter to the `where` object. ## TransactionIndex Indexes for querying Transactions. To optimize query performance and apply desired filters, choose the appropriate index. #### Values --- * ``TRANSACTION_ID`` * Index by `TRANSACTION_ID` field. Must supply a `transactionId: { eq: }` filter to the `where` object. --- * ``CORRELATION_ID`` * Index by `CORRELATION_ID` field. Must supply a `correlationId: { eq: }` filter to the `where` object. --- * ``EXTERNAL_ID`` * Index by `EXTERNAL_ID` field. Must supply an `externalId: { eq: }` filter to the `where` object. --- * ``CUSTOM`` * Use a custom-defined index. Must supply a `custom` filter to the `where` object. --- * ``SEARCH`` * Use eventually consisent search. Must supply a `search` filter to the `where` object. --- * ``TRANSACTION_IDS`` * Retrieve a list of transactions by id. Must supply an `ids` filter to the `where` object. --- * ``GROUP`` * Retrieve a list of transactions by group. Must supply a `group` filter to the `where` object. ## TxIsolation Transaction isolation level. Different isolation levels provide different guarantees about data visibility and consistency. #### Values --- * ``READ_COMMITTED`` * Read committed isolation - allows reading only committed data --- * ``SNAPSHOT`` * Snapshot isolation - provides a consistent snapshot view of the database --- * ``REPEATABLE_READ`` * Repeatable read isolation - prevents non-repeatable reads --- * ``SERIALIZABLE`` * Serializable isolation - strictest isolation level ## UploadType #### Values --- * ``BULK_GRAPHQL_VARIABLES`` * --- * ``ACH`` * ## VelocityControlIndex #### Values --- * ``VELOCITY_CONTROL_ID`` * Index by `velocityControlId` field. Must supply an `velocityControlId: { eq: }` filter to the `where` object. --- * ``NAME`` * Index by `name` field. Use to apply query filters on the value of `name`. --- * ``ACCOUNT_ID`` * Index by `accountId` attached to velocity control. Must supply an `accountId: { eq: }` filter to the `where` object. --- * ``VELOCITY_LIMIT_ID`` * ## VelocityEnforcementAction #### Values --- * ``WARN`` * Returns a selectable exception on postTransaction.exceptions, but allows transaction to be posted. --- * ``VOID`` * Returns a selectable exception on postTransaction.exceptions, and voids offending transaction. --- * ``REJECT`` * Returns an exception as an error, aborting entire request. ## VelocityLimitIndex #### Values --- * ``VELOCITY_LIMIT_ID`` * Index by `velocityLimitId` field. Must supply an `velocityLimitId: { eq: }` filter to the `where` object. --- * ``NAME`` * Index by `name` field. Use to apply query filters on the value of `name`. ## ViewEntity Enum of source tables that can trigger view updates. #### Values --- * ``Account`` * --- * ``AccountSet`` * --- * ``Balance`` * --- * ``Entry`` * --- * ``Transaction`` * --- * ``TranCode`` * ## ViewTriggerEnum #### Values --- * ``OnSelect`` * --- * ``OnInsert`` * --- * ``OnUpdate`` * --- * ``OnDelete`` * --- # Input Types Input types are used as arguments for mutations and queries to provide input data to an operation. Source: https://www.twisp.com/docs/reference/graphql/types/input ## AccountConfigInput Fields to create a system configuration for an account. #### Input Fields --- * ``enableConcurrentPosting`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, allow concurrent posting to the account. See `BalanceType` for balance retrieval options available for concurrent-enabled accounts. Defaults to `false`. --- * ``upsert`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true` use an upsert on the accountId index to upsert and avoid unique constraint violation. If account already created, the existing account is unchanged. --- * ``idempotent`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Enables idempotent account creation. When true, if an account with this ID already exists, the original creation arguments are compared against the request. If they match, the existing account is returned instead of failing with a unique constraint violation. Guarantees: - Idempotency is keyed on `accountId` — the caller must supply a stable ID. - The creation arguments (name, code, description, normalBalanceType, status, externalId, metadata, enableConcurrentPosting) must match the original creation. A mismatch fails with `BAD_REQUEST`. NOTE: `accountSetIds` memberships are NOT checked for idempotency. Cannot be used together with `upsert`. ## AccountEntriesFilterInput Filter conditions for entries on an Account. Since accountId is already known from the parent Account, only journalId and currency filtering is needed. #### Input Fields --- * ``journalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter by journal ID. If omitted, entries from all journals are returned. --- * ``currency`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter by currency. Defaults to USD if not specified. ## AccountFilterInput Filter conditions to apply to an account query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``accountId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `accountId` field. Required when using index `AccountIndex.ACCOUNT_ID`. --- * ``externalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `externalId` field. Required when using index `AccountIndex.EXTERNAL_ID`. --- * ``name`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `name` field. Only available when using index `AccountIndex.NAME`. --- * ``code`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `code` field. Only available when using index `AccountIndex.CODE`. --- * ``status`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `status` field. Only available when using index `AccountIndex.STATUS`. --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter conditions for a custom index. Only available when using index `AccountIndex.CUSTOM`. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Filter conditions for a search. Only available when using index `AccountIndex.SEARCH`. --- * ``codeUnique`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `code` field. Required when using index `AccountIndex.CODE_UNIQUE`. ## AccountIndexInput Specify the pre-defined AccountIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`AccountIndex!`](/docs/reference/graphql/types/enum#account-index) * Indexes for querying Accounts. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## AccountInput Fields to create a new account. #### Input Fields --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the account. --- * ``externalId`` - [`String`](/docs/reference/graphql/types/scalar#string) * Allows specifying a unique external ID associated with this account. --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Shorthand code for the account. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Account name. --- * ``normalBalanceType`` - [`DebitOrCredit!`](/docs/reference/graphql/types/enum#debit-or-credit) * Determines whether account should use a debit- or credit-normal balance. _Default:_ ``CREDIT`` --- * ``accountSetIds`` - [`[UUID]`]($gql:scalar:UUID) * IDs of AccountSets to add this account to. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the account. --- * ``status`` - [`Status!`](/docs/reference/graphql/types/enum#status) * Current status for the account. _Default:_ ``ACTIVE`` --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this account. --- * ``config`` - [`AccountConfigInput`](/docs/reference/graphql/types/input#account-config-input) * System config for the account. ## AccountSetConfigInput Fields to create a system configuration for an account set. #### Input Fields --- * ``enableConcurrentPosting`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, allow concurrent posting to the account. See `BalanceType` for balance retrieval options available for concurrent-enabled accounts. Defaults to `false`. --- * ``upsert`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true` use an upsert on the accountSetId index to upsert and avoid unique constraint violation. If account set already created, the existing account set is unchanged. --- * ``idempotent`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Enables idempotent account set creation. When true, if an account set with this ID already exists, the original creation arguments are compared against the request. If they match, the existing account set is returned instead of failing with a unique constraint violation. Guarantees: - Idempotency is keyed on `accountSetId` — the caller must supply a stable ID. - The creation arguments (name, description, journalId, normalBalanceType, code, metadata, enableConcurrentPosting) must match the original creation. A mismatch fails with `BAD_REQUEST`. NOTE: `accountSetIds` memberships are NOT checked for idempotency. Cannot be used together with `upsert`. ## AccountSetEntriesFilterInput Filter conditions for entries on an AccountSet. Since accountId and journalId are already known from the parent AccountSet, only currency filtering is needed. #### Input Fields --- * ``currency`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter by currency. Defaults to USD if not specified. ## AccountSetFilterInput Filter conditions to apply to an account set query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``accountSetId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `accountSetId` field. Required when using index `AccountSetIndex.ACCOUNT_SET_ID`. --- * ``journalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Specify the Journal to use with `eq`. Required for all indexes. --- * ``name`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `name` field. Only available when using index `AccountSetIndex.NAME`. --- * ``code`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Fitler on the `code` field. Only available when using index `AccountSetIndex.CODE`. --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter conditions for a custom index. Only available when using index `AccountSetIndex.CUSTOM`. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Filter conditions for a search. Only available when using index `AccountSetIndex.SEARCH`. ## AccountSetIndexInput Specify the pre-defined AccountSetIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`AccountSetIndex!`](/docs/reference/graphql/types/enum#account-set-index) * Indexes for querying AccountSets. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## AccountSetInput Fields to create a new account set. #### Input Fields --- * ``accountSetId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the set. --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * The journal for the set. If omitted, the default journal will be used. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name for the set. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the account set. --- * ``normalBalanceType`` - [`DebitOrCredit!`](/docs/reference/graphql/types/enum#debit-or-credit) * Determines whether the account set should use a debit- or credit-normal balance. _Default:_ ``CREDIT`` --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this account set. --- * ``config`` - [`AccountSetConfigInput`](/docs/reference/graphql/types/input#account-set-config-input) * System config for the account set. --- * ``accountSetIds`` - [`[UUID]`]($gql:scalar:UUID) * IDs of AccountSets to add this account set to. --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * Shorthand code for the account set. If not provided, a default code will be generated. ## AccountSetMemberInput #### Input Fields --- * ``memberType`` - [`AccountSetMemberType`](/docs/reference/graphql/types/enum#account-set-member-type) * Whether the member to add is an Account or AccountSet --- * ``memberId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Identifier for the member to add. When adding accounts, this is the `accountId`. When adding account sets, this is the `accountSetId`. ## AccountSetMembersFilterInput Filter conditions to apply when querying members of an account set. #### Input Fields --- * ``accountSetId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `accountSetId` field. --- * ``memberId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `memberId` field: the UUID of a member `Account` or `AccountSet`. ## AccountSetUpdateInput AccountSet fields to update. #### Input Fields --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name for the set. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the account set. --- * ``normalBalanceType`` - [`DebitOrCredit`](/docs/reference/graphql/types/enum#debit-or-credit) * Determines whether the account set should use a debit- or credit-normal balance. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this account set. --- * ``config`` - [`AccountSetConfigInput`](/docs/reference/graphql/types/input#account-set-config-input) * System config for the account set. --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * Shorthand code for the account set. ## AccountUpdateInput Account fields to update. #### Input Fields --- * ``externalId`` - [`String`](/docs/reference/graphql/types/scalar#string) * Allows specifying a unique external ID associated with this account. --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * Shorthand code for the account. --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * Account name. --- * ``normalBalanceType`` - [`DebitOrCredit`](/docs/reference/graphql/types/enum#debit-or-credit) * Determines whether account should use a debit- or credit-normal balance. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the account. --- * ``status`` - [`Status`](/docs/reference/graphql/types/enum#status) * Current status for the account. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this account. --- * ``config`` - [`AccountConfigInput`](/docs/reference/graphql/types/input#account-config-input) * System config for the account. ## AchCreateConfigurationInput #### Input Fields --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this configuration. --- * ``endpointId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Endpoint to use for decisioning this ACH file. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Journal to post settlements into. --- * ``settlementAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ACH Settlement Account. --- * ``exceptionAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account to post to when an exception occurs, such as a Velocity Control or Account in a locked state. Funds in this account will be returned. --- * ``suspenseAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account to post to when an account is not found. Funds in this account will be returned. --- * ``feeAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ACH Fee Account. --- * ``odfiHeaderConfiguration`` - [`AchOdfiHeaderConfigurationInput!`](/docs/reference/graphql/types/input#ach-odfi-header-configuration-input) * ACH Processor information for files. --- * ``timeZone`` - [`String!`](/docs/reference/graphql/types/scalar#string) * IANA Timezone identifier for the configuration. _Example:_ ``"America/Chicago"`` ## AchFileInfoFilterInput #### Input Fields --- * ``configId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``processingStatus`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``created`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` ## AchFileInfoIndexInput #### Input Fields --- * ``name`` - [`AchFileInfoIndex!`](/docs/reference/graphql/types/enum#ach-file-info-index) * ## AchGenerateFileInput #### Input Fields --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The configuration to use for generating return file. --- * ``fileKey`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The key to store this file. --- * ``fileType`` - [`AchFileType!`](/docs/reference/graphql/types/enum#ach-file-type) * The type of file to generate. --- * ``generateEmpty`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * If true, generate empty files. Defaults to true. _Default:_ ``true`` ## AchOdfiHeaderConfigurationInput #### Input Fields --- * ``immediateDestination`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateDestinationName`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateOrigin`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateOriginName`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## AchProcessFileInput #### Input Fields --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Configuration to use to process this file. --- * ``fileKey`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The file key to use for this file. --- * ``fileType`` - [`AchFileType!`](/docs/reference/graphql/types/enum#ach-file-type) * The type of file being processed. --- * ``options`` - [`AchProcessFileOptionsInput`](/docs/reference/graphql/types/input#ach-process-file-options-input) * Additional ACH processing options. ## AchProcessFileOptionsInput #### Input Fields --- * ``preprocessedFileKey`` - [`String`](/docs/reference/graphql/types/scalar#string) * Output file key for preprocessed files. --- * ``preprocessedExcludedFileKey`` - [`String`](/docs/reference/graphql/types/scalar#string) * Optionally output file key for excluded preprocessed entries. ## AchUpdateConfigurationInput #### Input Fields --- * ``endpointId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Endpoint to use for decisioning this ACH file. --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Journal to post settlements into. --- * ``settlementAccountId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * ACH Settlement Account. --- * ``exceptionAccountId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Account to post to when an exception occurs, such as a Velocity Control or Account in a locked state. Funds in this account will be returned. --- * ``suspenseAccountId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Account to post to when an account is not found. Funds in this account will be returned. --- * ``feeAccountId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * ACH Fee Account. --- * ``odfiHeaderConfiguration`` - [`AchOdfiHeaderConfigurationInput`](/docs/reference/graphql/types/input#ach-odfi-header-configuration-input) * ACH Processor information for files. **Note:** replaces existing `odfiHeaderConfiguration` --- * ``timeZone`` - [`String`](/docs/reference/graphql/types/scalar#string) * IANA Timezone identifier for the configuration. _Example:_ ``"America/Chicago"`` ## AttachCalculationInput #### Input Fields --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Journal to attach calculation to. This parameter is ignored when attaching to an account set. Defaults to the default journal if not provided when attaching to an account. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account or Account Set to attach calculation to. --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Calculation to attach. ## BalanceFilterInput Filter conditions to apply to a balance query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``journalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Specify the Journal to use with `eq`. If omitted, the default journal will be used. --- * ``accountId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `accountId` field. Required when using index `BalanceIndex.ACCOUNT_ID`. --- * ``currency`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `currency` field. --- * ``calculationId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on `calculationId` field when using `BalanceIndex.DIMENSION` --- * ``dimension`` - [`DimensionFilterValue`](/docs/reference/graphql/types/input#dimension-filter-value) * Filter on `dimension` field when using `BalanceIndex.Dimension` --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter conditions for a custom index. Only available when using index `BalanceIndex.CUSTOM`. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Filter conditions for a search. Only available when using index `BalanceIndex.SEARCH`. ## BalanceHistoryFilterInput Filter conditions to apply to a balance history query. #### Input Fields --- * ``modified`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `modified` timestamp. --- * ``committed`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the transaction commit timestamp for the specific balance record version. ## BalanceIndexInput Specify the pre-defined BalanceIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`BalanceIndex!`](/docs/reference/graphql/types/enum#balance-index) * Indexes for querying Balances. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## BalanceLimitInput #### Input Fields --- * ``layer`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The layer this balance limit is enforced at. Must resolve to `SETTLED`, `PENDING` or `ENCUMBRANCE`. --- * ``amount`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The maximum amount at this layer that can be spent. Must resolve to a decimal. --- * ``normalBalanceType`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The direction this balance enforces on as an upper limit. Must resolve to `CREDIT` or `DEBIT`. --- * ``start`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * The timestamp at which this balance limit begins to be effective. If provided, must resolve to a `timestamp`. Defaults to the creation stamp of the underlying control. @example("timestamp('2022-01-01T14:00:00.000Z')") --- * ``end`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * The timestamp at which this balance limit ceases to be effective. If provided, must resolve to a `timestamp`. Defaults to infinite timestamp. @example("timestamp('2022-01-01T15:00:00.000Z')") ## BatchExecuteStatementInput #### Input Fields --- * ``sqls`` - [`[String]`]($gql:scalar:String) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Between #### Input Fields --- * ``begin`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``end`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## BulkQueryExecutionFilterInput Filter conditions to apply to a bulk execution query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``status`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `status` field. Required when using index `BulkQueryExecutionIndex.STATUS`. --- * ``created`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `created` field. Available as sort key on `BulkQueryExecutionIndex.STATUS`. ## BulkQueryExecutionIndexInput Specify the pre-defined BulkQueryExecutionIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`BulkQueryExecutionIndex!`](/docs/reference/graphql/types/enum#bulk-query-execution-index) * Indexes for querying bulk executions. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## BulkQueryInput #### Input Fields --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * Human readable name of this bulk execution. _Default:_ ``"Bulk Query"`` --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Key to file to execute. --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique Identifier for this execution of a bulk query. --- * ``query`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Parameterized GraphQL query string to execute. --- * ``transform`` - [`String`](/docs/reference/graphql/types/scalar#string) * Optional jq transform to run on variables in file identified by `key`. The result of the transform must be a list of json variables. @example("map(.accountId)") ## CalculationConfigInput #### Input Fields --- * ``enableEffectiveBalances`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Enable effective date calculations. Creates 3 child calculations for YYYY, YYYY_MM, and YYYY_MM_DD. Effective date dimensions are prepended to custom dimensions. _Default:_ ``false`` --- * ``effectiveDateSource`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * CEL expression resolving to a date, used as the base for effective date dimensions. Defaults to context.vars.transaction.effective when empty. Requires enableEffectiveBalances to be true. ## CalculationFilterInput #### Input Fields --- * ``calculationId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``code`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``status`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` ## CalculationIndexInput #### Input Fields --- * ``name`` - [`CalculationIndex!`](/docs/reference/graphql/types/enum#calculation-index) * --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## CalculationUpdateInput #### Input Fields --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * Unique shorthand code for this calculation. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Human readable description of this calculation. ## CancelStatementInput #### Input Fields --- * ``id`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## CardInitializeInput #### Input Fields --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * The unique identifier for the journal that card transactions will post to by default. If omitted, the default journal will be used. --- * ``settlementAccountId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * A unique identifier to an existing settlement account that card transactions will post to by default. If not provided, a default card transaction account will be used as the settlement account. ## CreateCalculationInput #### Input Fields --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this calculation. --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique shorthand code for this balance calculation. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable description of this calculation. --- * ``scope`` - [`CalculationScope!`](/docs/reference/graphql/types/enum#calculation-scope) * The calculation scope of this calculation. Defaults to `GLOBAL`. _Default:_ ``GLOBAL`` --- * ``dimensions`` - [`[PartitionKeyInput]!`]($gql:input:PartitionKeyInput) * Group by these values to index the calculation. The `account`, `transaction` and `entry` are available for use in the dimension computation on `context.vars`. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if an balance entry should be written. The `account`, `transaction`, `tranCode` and `entry` are available for use in the dimension computation on `context.vars`. @example("has(context.vars.account.metadata.policyPayment)") --- * ``config`` - [`CalculationConfigInput`](/docs/reference/graphql/types/input#calculation-config-input) * Configuration options for this calculation. ## CreateClientInput #### Input Fields --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Principal that this client applies to. If you're supplying your own OIDC this will be the `iss` claim on your JWT. If using Twisp IAM/OIDC token exchange, this will be the IAM principal you signed with, typically a role ARN. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique name of the client. --- * ``policies`` - [`[PolicyInput]!`]($gql:input:PolicyInput) * The policies to evaluate. ## CreateGroupInput #### Input Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the group. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A human-friendly name for the group, such as 'Admins' or 'DataAnalysts'. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A brief description of the group's purpose, intended to provide additional context. --- * ``policy`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A set of policies to apply to this group, formatted as a JSON list that define the permissions granted to users within this group. The structure of these policies matches the Policy type, but serialized as a JSON string. Example: ``` policy: "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"],\"assertions\": {\"always false\": \"1 == 0\"}}]" ``` ## CreateIndexInput #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``on`` - [`IndexOnEnum!`](/docs/reference/graphql/types/enum#index-on-enum) * The type of record this index applies to. --- * ``viewName`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name of the target view. Required when `on: View`; ignored otherwise. --- * ``async`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is populated asynchronously. --- * ``unique`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is unique. --- * ``partition`` - [`[PartitionKeyInput]!`]($gql:input:PartitionKeyInput) * The partition key used for this index. --- * ``partitionShardCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * Specifies the number of shards for partition write scaling. This parameter defines how many shards the partition key is automatically split into, similarly to RAID-style disk striping. Increasing this value allows the index to distribute write throughput across multiple shards while sacrificing global sort order on the partition. For instance, setting `partitionShardCount` to 4 splits each unique partition into four shards, effectively allowing 4000 writes per second for a single partition key. --- * ``sort`` - [`[IndexKeyInput]!`]($gql:input:IndexKeyInput) * The sort key to use for supporting range queries. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCategory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. ## CreateScheduleInput #### Input Fields --- * ``jobType`` - [`JobType!`](/docs/reference/graphql/types/enum#job-type) * The job type to create the schedule for. Currently only one schedule per job-type is supported. --- * ``jobName`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A job name that's unique per job type. --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The Twisp principal to run the job on a schedule. This should have a matching `client` policy. --- * ``scheduleExpression`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A schedule expression to run this job on. cron/rate/once supported see https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html for valid syntax. --- * ``timezone`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The timezone to run this schedule on based on https://www.iana.org/time-zones example: "America/Los_Angeles" or "UTC" Supports ST rules defined in https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html --- * ``metadata`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * JSON metadata to pass to running job. ## CreateSearchIndexInput #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``on`` - [`IndexOnEnum!`](/docs/reference/graphql/types/enum#index-on-enum) * The type of record this index applies to. --- * ``viewName`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name of the target view. Required when `on: View`; ignored otherwise. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCategory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. --- * ``opensearchSchema`` - [`OpensearchSchemaInput!`](/docs/reference/graphql/types/input#opensearch-schema-input) * Opensearch mapping (with CEL expressions) applied to the document prior to indexing. Required. Every search index must declare its field types explicitly so that sort, filter, and aggregation behavior is stable across index creations. Indexes created without a schema rely on Opensearch dynamic mapping defaults, which have drifted over time and across Opensearch Serverless versions, producing cursors and sort behaviors that are not portable between indexes — so this path is no longer supported. ## CreateTenantInput #### Input Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the tenant. --- * ``accountId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A globally unique identifier representing an environment within the organization. This accountId, when combined with an AWS region, is used to calculate the database tenant. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A human-friendly name for the tenant, used for display purposes and easier identification. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A brief description of the tenant, providing additional context about its purpose or characteristics. ## CreateUpload #### Input Fields --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of file. e.g. `path/to/file.json` --- * ``uploadType`` - [`UploadType!`](/docs/reference/graphql/types/enum#upload-type) * The type of upload: `BULK_GRAPHQL_VARIABLES` - The content-type is `application/json` and is an array of json objects for a bulk graphql query execution. `ACH` - The content-type is `text/plain` and is a NACHA formatted text file. _Default:_ ``BULK_GRAPHQL_VARIABLES`` --- * ``contentType`` - [`String!`](/docs/reference/graphql/types/scalar#string) * `contentType` of file. --- * ``contentEncoding`` - [`String`](/docs/reference/graphql/types/scalar#string) * `contentEncoding` of file. Only `gzip` is supported. Only allowed for `BULK_GRAPHQL_VARIABLES` uploads. ## CreateUserInput #### Input Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the user. --- * ``groupIds`` - [`[UUID!]!`]($gql:scalar:UUID) * A list of unique identifiers for the groups to which the user belongs. The user's permissions are determined by the combined policies of these groups. --- * ``email`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The user's email address, which serves as a unique identifier and primary means of contact. ## CreateViewInput Input for creating a new view materialized view. #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this view. Will be used as the table name in the public namespace. Should be a short, human-readable name. --- * ``document`` - [`[DocumentElementInput!]!`]($gql:input:DocumentElementInput) * Cel Expressions and Type that define the aggregation. `context.source` will have the triggering document. --- * ``sources`` - [`[ViewSourceInput!]!`]($gql:input:ViewSourceInput) * List of source tables that trigger updates to this view. When records in these tables are inserted, updated, or deleted, the view will be recalculated. --- * ``partition`` - [`[PartitionKeyInput!]!`]($gql:input:PartitionKeyInput) * Partition for this aggregation. Use `context.source` for defining the index. --- * ``sort`` - [`[IndexKeyInput!]!`]($gql:input:IndexKeyInput) * Sort for this aggregation. Use `context.source` for defining the index. --- * ``filters`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions that filter when the view should be updated. Only source changes that satisfy all filter conditions will trigger an update to the view. Each expression must return a boolean value. Default: { enabled: "true" } (all changes trigger an update) --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable description of this view. _Default:_ ``""`` --- * ``config`` - [`ViewConfigInput`](/docs/reference/graphql/types/input#view-config-input) * Extra config options for view. --- * ``normalize`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A CEL expression to normalize the source object by. If provided, must evaluate to a list. The trigger will be repeated for each item in the list and the value will be available on `context.trigger.normalize`. If evaluates to an empty list, will behave as if the normalize expression not provided. --- * ``indexes`` - [`[ViewIndexInput]`]($gql:input:ViewIndexInput) * --- * ``searchIndexes`` - [`[ViewSearchIndexInput]`]($gql:input:ViewSearchIndexInput) * ## CustomIndexFilter Query conditions for a custom index. #### Input Fields --- * ``index`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `name` of the custom index to use. --- * ``partition`` - [`[CustomIndexFilterValue]`]($gql:input:CustomIndexFilterValue) * Query conditions for specifying the index partition to use. --- * ``sort`` - [`[CustomIndexFilterValue]`]($gql:input:CustomIndexFilterValue) * Query conditions for specifying sort order. ## CustomIndexFilterValue Filter conditionals for querying the partition or sort key of a custom index. #### Input Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Identifier for the key to apply the filter to. --- * ``value`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditions to apply at this key. ## DescribeStatementInput #### Input Fields --- * ``id`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## DescribeTableInput #### Input Fields --- * ``database`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``maxResults`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``schema`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``table`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## DestinationInput #### Input Fields --- * ``files`` - [`FilesDestinationInput!`](/docs/reference/graphql/types/input#files-destination-input) * ## DetachCalculationInput #### Input Fields --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Journal to detach calculation. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account or set to detach calculation. --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Calculation to detach. ## DimensionBetween #### Input Fields --- * ``begin`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``end`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. ## DimensionFilterValue #### Input Fields --- * ``eq`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``like`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``lt`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``lte`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``gt`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``gte`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``between`` - [`DimensionBetween`](/docs/reference/graphql/types/input#dimension-between) * ## DocumentElementInput #### Input Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Alias for this element. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The CEL expression for the value of this document element. _Example:_ ``"context.source.account_id"`` --- * ``type`` - [`CelType!`](/docs/reference/graphql/types/enum#cel-type) * The type this document element resolves to. --- * ``listOfType`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * If `true` resolves the type as a list of `type` (i.e. `[type]`) _Default:_ ``false`` ## Effective #### Input Fields --- * ``cumulative`` - [`Date`](/docs/reference/graphql/types/scalar#date) * Cumulative account balance as of a particular effective date. _Example:_ ``"2023-10-17"`` --- * ``period`` - [`String`](/docs/reference/graphql/types/scalar#string) * Account balance of a particular effective time period. Supported periods are in formats: `YYYY`, `YYYY-MM`, `YYYY-MM-DD`. Useful for reconciliation/balances files. _Example:_ ``"2023-10"`` --- * ``range`` - [`PeriodRange`](/docs/reference/graphql/types/input#period-range) * Effective account balances over a range of time periods. Useful for income statement. _Example:_ ``{gte: "2023-10", lte: "2023-12"}`` --- * ``periods`` - [`Periods`](/docs/reference/graphql/types/input#periods) * Per-period balances across the half-open interval `[gte, lt)`. Granularity is inferred from the format of `gte`/`lt` (`YYYY`, `YYYY-MM`, or `YYYY-MM-DD`); both bounds must use the same format and `lt` must be strictly greater than `gte`. With `accumulate: false` (default), each `effectiveBalances` entry is that period's own activity and the top-level balance is the sum across the range — useful for income-statement-style queries. With `accumulate: true`, each entry is the cumulative balance through the end of its period and the top-level balance is the closing cumulative just before `lt` — useful for daily statement snapshots and "as-of" queries. The opening balance for the range can be recovered either by querying the prior period with `cumulative`, or by subtracting the first entry's activity from its cumulative. _Example:_ ``{gte: "2024-03-01", lt: "2024-04-01", accumulate: true}`` --- * ``where`` - [`BalanceHistoryFilterInput`](/docs/reference/graphql/types/input#balance-history-filter-input) * Optional point-in-time filter for resolving historical effective balances. ## EndpointFilterInput #### Input Fields --- * ``endpointId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `endpointId` field. Required when using index `EndpointInded.ENDPOINT_ID`. --- * ``status`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `status` field. Only available when using index `EndpointIndex.STATUS`. ## EndpointIndexInput #### Input Fields --- * ``name`` - [`EndpointIndex!`](/docs/reference/graphql/types/enum#endpoint-index) * --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## EndpointInput Fields to create a new endpoint. #### Input Fields --- * ``endpointId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the endpoint. --- * ``status`` - [`EndpointStatus`](/docs/reference/graphql/types/enum#endpoint-status) * Current status of the endpoint. _Default:_ ``ENABLED`` --- * ``endpointType`` - [`EndpointType`](/docs/reference/graphql/types/enum#endpoint-type) * The type of endpoint this endpoint is. _Default:_ ``WEBHOOK`` --- * ``url`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The url where this endpoint will transmit subscribed events to. --- * ``subscription`` - [`[String!]!`]($gql:scalar:String) * A list of subscriptions that are available, supporting wildcards `*`. Format: `.` Supported entities: - journal - account - accountcontext - accountset - accountsetmember - trancode - transaction - entry - balance - customindex - custombalance - endpoint --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * description of this endpoint. --- * ``filters`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying conditions for sending an event to the endpoint. Record is only sent if _all_ expressions evaluate to true, i.e. they are combined with a logical AND. Each expression must return a boolean value. ## EndpointUpdateInput Fields to update an existing endpoint. #### Input Fields --- * ``status`` - [`EndpointStatus`](/docs/reference/graphql/types/enum#endpoint-status) * Current status of the endpoint. --- * ``endpointType`` - [`EndpointType`](/docs/reference/graphql/types/enum#endpoint-type) * The type of endpoint this endpoint is. --- * ``url`` - [`String`](/docs/reference/graphql/types/scalar#string) * The url where this endpoint will transmit subscribed events to. --- * ``subscription`` - [`[String]`]($gql:scalar:String) * A list of subscriptions that are available, supporting wildcards `*`. Format: `.` Supported entities: - journal - account - accountcontext - accountset - accountsetmember - trancode - transaction - entry - balance - customindex - custombalance - endpoint --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * description of this endpoint. --- * ``filters`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying conditions for sending an event to the endpoint. Record is only sent if _all_ expressions evaluate to true, i.e. they are combined with a logical AND. Each expression must return a boolean value. ## EntryFilterInput Filter conditions to apply to an entry query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``entryId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `entryId` field. Required when using index `EntryIndex.ENTRY_ID`. --- * ``journalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``currency`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``layer`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` --- * ``transactionId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `transactionId` field. Required when using index `EntryIndex.TRANSACTION_ID`. --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter conditions for a custom index. Only available when using index `EntryIndex.CUSTOM`. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Filter conditions for a search. Only available when using index `EntryIndex.SEARCH`. ## EntryIndexInput Specify the pre-defined EntryIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`EntryIndex!`](/docs/reference/graphql/types/enum#entry-index) * Indexes for querying Entries. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## EntryUpdateInput Entry fields to update. #### Input Fields --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the entry. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this entry. ## ExecuteStatementInput #### Input Fields --- * ``sql`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ExecuteStatementSyncInput #### Input Fields --- * ``sql`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ExportInput #### Input Fields --- * ``entity`` - [`ExportEntity!`](/docs/reference/graphql/types/enum#export-entity) * Which Entity to export. --- * ``version`` - [`ExportVersion!`](/docs/reference/graphql/types/enum#export-version) * If `HISTORICAL`, exports every version of the entity. If `LATEST`, exports the latest version of the entity. --- * ``format`` - [`ExportFormat!`](/docs/reference/graphql/types/enum#export-format) * Output format for the export. Reccomend JSON or Parquet. --- * ``compression`` - [`ExportCompression!`](/docs/reference/graphql/types/enum#export-compression) * Compression options for the export. Recommend picking one, for lower read units. --- * ``destination`` - [`DestinationInput!`](/docs/reference/graphql/types/input#destination-input) * Destination for export. Currently only files API supported. --- * ``formatOptions`` - [`FormatOptions`](/docs/reference/graphql/types/input#format-options) * Additional formatting options for export files. --- * ``fromTimestamp`` - [`Timestamp`](/docs/reference/graphql/types/scalar#timestamp) * Optionally define export from timestamp. Returns records which were created with `timestamp >= fromTimestamp` Used together with `toTimestamp` is the half open interval `fromTimestamp >= timestamp && timestamp < toTimestamp`. --- * ``toTimestamp`` - [`Timestamp`](/docs/reference/graphql/types/scalar#timestamp) * Optionall define export to timestamp. Returns records which were created with `timestamp < toTimestamp` Used together with `fromTimestamp` is the half open interval `fromTimestamp >= timestamp && timestamp < toTimestamp`. ## FilesDestinationInput #### Input Fields --- * ``keyPrefix`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## FilterValue Conditional logic by which to apply a filter on a query. Each FilterValue object must contain just one key/value pair. Valid: `{ eq: "123" }`\ Invalid: `{ eq: "123", gt: "100" }` #### Input Fields --- * ``eq`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``like`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``lt`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``lte`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``gt`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``gte`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``between`` - [`Between`](/docs/reference/graphql/types/input#between) * ## FormatOptions #### Input Fields --- * ``nullAs`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``header`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``delimiter`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## GetStatementResultInput #### Input Fields --- * ``id`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ISO8583CreateConfigInput #### Input Fields --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this configuration. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Journal to post settlements into. --- * ``settlementAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ISO8583 Settlement Account. --- * ``timeZone`` - [`String!`](/docs/reference/graphql/types/scalar#string) * IANA Timezone identifier for the configuration. _Example:_ ``"America/Chicago"`` --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of this configuration. --- * ``processor`` - [`ISO8583ProcessorType!`](/docs/reference/graphql/types/enum#iso8583processor-type) * Processor type for this configuration. --- * ``processorSpec`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Processor configuration. --- * ``openToBuyConfig`` - [`OpenToBuyConfigInput`](/docs/reference/graphql/types/input#open-to-buy-config-input) * Balance to use for balance inquiry and partial authorizations. ## ISO8583UpdateConfigInput #### Input Fields --- * ``timeZone`` - [`String`](/docs/reference/graphql/types/scalar#string) * IANA Timezone identifier for the configuration. _Example:_ ``"America/Chicago"`` --- * ``openToBuyConfig`` - [`OpenToBuyConfigInput`](/docs/reference/graphql/types/input#open-to-buy-config-input) * Balance to use for balance inquiry and partial authorizations. --- * ``processor`` - [`ISO8583ProcessorType`](/docs/reference/graphql/types/enum#iso8583processor-type) * Processor type for this configuration. --- * ``processorSpec`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Processor configuration. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of this configuration. ## IndexKeyInput Specify a named expression to sort the records within a custom index. Used for sorting and for querying by range conditions. #### Input Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Identifier for this key. Should be a short, human-readable name. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression which resolves to the value that is to be sorted. Within the expression, the `document` object represents the record. To sort by a field on the record, use `document.`. --- * ``sort`` - [`SortOrder!`](/docs/reference/graphql/types/enum#sort-order) * Whether the sort is in ascending or descending order. --- * ``type`` - [`IndexDataType`](/docs/reference/graphql/types/enum#index-data-type) * Optionally provide explicit type for value. Useful for metadata values which may be list of monomorphic types. _Example:_ ``"type: STRING"`` ## JournalConfigInput Fields to create a system configuration for a journal. #### Input Fields --- * ``enableEffectiveBalances`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, records point-in-time effective balances for all accounts in the journal. Defaults to `false`. ## JournalFilterInput Filter conditions to apply to a journal query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``journalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `journalId` field. Required when using index `JournalIndex.JOURNAL_ID`. --- * ``name`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `name` field. Only available when using index `JournalIndex.NAME`. --- * ``status`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `status` field. Only available when using index `JournalIndex.STATUS`. --- * ``code`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `code` field. Only available when using index `JournalIndex.CODE`. ## JournalIndexInput Specify the pre-defined JournalIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`JournalIndex!`](/docs/reference/graphql/types/enum#journal-index) * Indexes for querying Journals. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## JournalInput Fields to create a new Journal. #### Input Fields --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the journal. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name for the journal. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the journal. --- * ``status`` - [`Status!`](/docs/reference/graphql/types/enum#status) * Operational status of the journal. _Default:_ ``ACTIVE`` --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * Optional unique code for the journal. --- * ``config`` - [`JournalConfigInput`](/docs/reference/graphql/types/input#journal-config-input) * System config for the journal. ## JournalUpdateInput Journal fields to update. #### Input Fields --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name for the journal. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the journal. --- * ``status`` - [`Status`](/docs/reference/graphql/types/enum#status) * Operational status of the journal. --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * The code used to refer to this journal. ## KVFilterInput #### Input Fields --- * ``namespace`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the built-in `namespace` field. --- * ``key`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the built-in `key` field. --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter on a custom index. ## KVIndexInput #### Input Fields --- * ``name`` - [`KVIndex!`](/docs/reference/graphql/types/enum#kvindex) * Index to query. --- * ``sort`` - [`SortOrder!`](/docs/reference/graphql/types/enum#sort-order) * Sort order within the selected index. _Default:_ ``ASC`` ## KVPutInput #### Input Fields --- * ``namespace`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Namespace for the record. Max 512 UTF-8 bytes. --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Key within the namespace. Max 512 UTF-8 bytes. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Optional description. Counts toward the 256 KiB persisted payload budget. --- * ``value`` - [`Value!`](/docs/reference/graphql/types/scalar#value) * JSON payload to store. Together with `description`, must be <= 256 KiB. --- * ``conditions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * CEL conditions evaluated against the request context before the write is applied. `document` and `value` are bound to the current KV record (the version about to be replaced), or `null` when no record exists yet — so a create-only guard can be written as `document == null`. Condition expressions are persisted on the written `KVValue` for auditing, keyed by the map key. ## KVUpdateInput #### Input Fields --- * ``namespace`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Namespace for the record. Max 512 UTF-8 bytes. --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Key within the namespace. Max 512 UTF-8 bytes. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * When set, replaces the stored description on the written version. When omitted, the existing description is preserved. Counts toward the 256 KiB persisted payload budget. --- * ``expressions`` - [`ExpressionValue!`](/docs/reference/graphql/types/scalar#expression-value) * Expression-valued RFC 7396 merge patch. String leaves are CEL expressions; non-string leaves are literal patch values. --- * ``conditions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * CEL conditions evaluated before the write. `document` and `value` are bound to the current KV record; `UpdateKv` fails with `NOT_FOUND` before conditions run when the record does not exist, so `document` is always non-null here. Condition expressions are persisted on the written `KVValue` for auditing, keyed by the map key. ## LimitInput #### Input Fields --- * ``timestampSource`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Uses a timestamp from the specified source for picking the balance limit. By default uses the system `transaction.timestamp`. Must resolve to a CEL `timestamp`. @example("timestamp(context.vars.transaction.?metadata.ts.orValue(context.transaction.timestamp))") --- * ``balance`` - [`[BalanceLimitInput!]!`]($gql:input:BalanceLimitInput) * ## ListDatabasesInput #### Input Fields --- * ``maxResults`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ListSchemasInput #### Input Fields --- * ``maxResults`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``schemaPattern`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``database`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ListStatementsInput #### Input Fields --- * ``maxResults`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``statementName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``status`` - [`SqlStatementStatus!`](/docs/reference/graphql/types/enum#sql-statement-status) * ## ListTablesInput #### Input Fields --- * ``maxResults`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``database`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``schemaPattern`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``tablePattern`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## LithicTransactionInput #### Input Fields --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the account this transaction will post to. --- * ``webhook`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * The [Transaction](https://docs.lithic.com/docs/transactions) webhook object from Lithic. --- * ``journalId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the journal this transaction applies to. If not provided, defaults to the default journal that card transaction codes are configured with. --- * ``settlementAccountId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier of the settlement account that transactions will settle from. If not provided, defaults to the default card settlement account. ## MoneyFormatInput Formatting options for money amounts. #### Input Fields --- * ``locale`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Locale represents a Unicode locale identifier. _Examples:_ ``'de-DE'``, ``'hi-IN'`` --- * ``groupDigits`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * When true, whole digits will be grouped according to locale. For example, with locale `en-US` the number `1234567.89` is formatted with grouped digits as `1,234,567.89`. With other locales, these groupings may apply differently. _Default:_ ``false`` --- * ``addPlusSign`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * When true, prefix the number with plus `+` symbol when the number is positive. Negative numbers are always displayed with a minus `-` symbol. _Default:_ ``false`` --- * ``roundingMode`` - [`RoundingMode!`](/docs/reference/graphql/types/enum#rounding-mode) * Defines the rounding behavior when the fractional units exceed the `maxDigits`. _Default:_ ``HALF_UP`` --- * ``currencyDisplay`` - [`CurrencyDisplay!`](/docs/reference/graphql/types/enum#currency-display) * Defines how to render the currency indicator. _Default:_ ``SYMBOL`` --- * ``minDigits`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Minimum number of fractional digits. When not specified, it will use the default fractional digits for the currency. For example, USD amounts default to 2 minimum digits. _Default:_ ``255`` --- * ``maxDigits`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Maximum number of fractional digits to show, which informs how rounding behavior is applied via the `roundingMode`. Defaults to 6. _Default:_ ``6`` ## MoneyInput #### Input Fields --- * ``units`` - [`Decimal!`](/docs/reference/graphql/types/scalar#decimal) * Decimal is a fixed-precision data type supporting exact representation of numeric values. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * ISO 4217 standard three-character code indicating the currency. ## OpenToBuyConfigInput #### Input Fields --- * ``openToBuy`` - [`OpenToBuyType!`](/docs/reference/graphql/types/enum#open-to-buy-type) * _Default:_ ``VELOCITY_BALANCE`` ## OpensearchSchemaBinaryMappingInput #### Input Fields --- * ``type`` - [`OpensearchSchemaBinaryMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-binary-mapping-type) * --- * ``celExpression`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The CEL expression to index for the document. _Example:_ ``"document.is_void"`` ## OpensearchSchemaBooleanMappingInput #### Input Fields --- * ``type`` - [`OpensearchSchemaBooleanMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-boolean-mapping-type) * --- * ``celExpression`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * A literal CEL expression to be evaluated. ## OpensearchSchemaDateMappingInput #### Input Fields --- * ``type`` - [`OpensearchSchemaDateMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-date-mapping-type) * --- * ``celExpression`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The CEL expression to index for the document. _Example:_ ``"document.effective"`` ## OpensearchSchemaInput #### Input Fields --- * ``mappings`` - [`OpensearchSchemaObjectMappingInput!`](/docs/reference/graphql/types/input#opensearch-schema-object-mapping-input) * ## OpensearchSchemaMappingInput Use one of `binaryType`, `numericType`, `booleanType`,`dateType`, `objectType` or `stringType`. #### Input Fields --- * ``binaryType`` - [`OpensearchSchemaBinaryMappingInput`](/docs/reference/graphql/types/input#opensearch-schema-binary-mapping-input) * --- * ``numericType`` - [`OpensearchSchemaNumericMappingInput`](/docs/reference/graphql/types/input#opensearch-schema-numeric-mapping-input) * --- * ``booleanType`` - [`OpensearchSchemaBooleanMappingInput`](/docs/reference/graphql/types/input#opensearch-schema-boolean-mapping-input) * --- * ``dateType`` - [`OpensearchSchemaDateMappingInput`](/docs/reference/graphql/types/input#opensearch-schema-date-mapping-input) * --- * ``objectType`` - [`OpensearchSchemaObjectMappingInput`](/docs/reference/graphql/types/input#opensearch-schema-object-mapping-input) * --- * ``stringType`` - [`OpensearchSchemaStringMappingInput`](/docs/reference/graphql/types/input#opensearch-schema-string-mapping-input) * ## OpensearchSchemaMappingsInput #### Input Fields --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``value`` - [`OpensearchSchemaMappingInput!`](/docs/reference/graphql/types/input#opensearch-schema-mapping-input) * Use one of `binaryType`, `numericType`, `booleanType`,`dateType`, `objectType` or `stringType`. ## OpensearchSchemaMultiFieldInput #### Input Fields --- * ``stringType`` - [`OpensearchSchemaStringMultiFieldInput`](/docs/reference/graphql/types/input#opensearch-schema-string-multi-field-input) * ## OpensearchSchemaMultiFieldsInput #### Input Fields --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``value`` - [`OpensearchSchemaMultiFieldInput!`](/docs/reference/graphql/types/input#opensearch-schema-multi-field-input) * ## OpensearchSchemaNumericMappingInput #### Input Fields --- * ``type`` - [`OpensearchSchemaNumericMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-numeric-mapping-type) * --- * ``celExpression`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The CEL expression to index for the document. @example("document.amount.units()") ## OpensearchSchemaObjectMappingInput #### Input Fields --- * ``type`` - [`OpensearchSchemaObjectMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-object-mapping-type) * --- * ``properties`` - [`[OpensearchSchemaMappingsInput]`]($gql:input:OpensearchSchemaMappingsInput) * ## OpensearchSchemaStringMappingInput #### Input Fields --- * ``type`` - [`OpensearchSchemaStringMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-string-mapping-type) * --- * ``celExpression`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The CEL expression to index for the document. _Example:_ ``"document.description"`` --- * ``fields`` - [`[OpensearchSchemaMultiFieldsInput]`]($gql:input:OpensearchSchemaMultiFieldsInput) * Provide multi-field mappings for this field. ## OpensearchSchemaStringMultiFieldInput #### Input Fields --- * ``type`` - [`OpensearchSchemaStringMappingType!`](/docs/reference/graphql/types/enum#opensearch-schema-string-mapping-type) * ## ParamDefinitionInput Define a parameter that can be used when posting transactions using this tran code. #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name for the parameter. This is how values passed are accessed. For example, a parameter with name `fromAccount` can be accessed in the `accountId` field of an TranCodeEntryInput with `params.fromAccount`. --- * ``type`` - [`ParamDataType!`](/docs/reference/graphql/types/enum#param-data-type) * Data type for the parameter. _Default:_ ``STRING`` --- * ``default`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Default value for the parameter. If not provided, the parameter is consider a 'required' parameter, and a value must be provided when posting a transaction. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Describe the purpose of this parameter. Help an engineer out. --- * ``example`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Example value used for type-checking CEL expressions at tran code creation time. Does NOT provide a runtime default — use `default` for that. ## PartitionKeyInput Specify a named expression to define a partition key. #### Input Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Identifier for this partition key. Should be a short, human-readable name. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression which resolves to the value that is to be used for the partition key. Within the expression, the `document` object represents the record. To access a field on the document, use `document.`. --- * ``type`` - [`IndexDataType`](/docs/reference/graphql/types/enum#index-data-type) * Optionally provide explicit type for value. Useful for metadata values which may be list of monomorphic types. _Example:_ ``"type: STRING"`` ## PeriodRange Closed-closed period range `[gte, lte]` used by the `range` effective variant. #### Input Fields --- * ``lte`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``gte`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Periods Half-open enumeration of periods `[gte, lt)` used by the `periods` effective variant. `gte` is inclusive, `lt` is exclusive, and `lt` must be strictly greater than `gte`. #### Input Fields --- * ``gte`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``lt`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``accumulate`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, each `effectiveBalances` entry is the cumulative balance through the end of its period and the top-level balance is the closing cumulative just before `lt`. When `false` (default), each entry is that period's own activity and the top-level balance is the sum across the range. _Default:_ ``false`` ## PolicyInput #### Input Fields --- * ``effect`` - [`PolicyEffect!`](/docs/reference/graphql/types/enum#policy-effect) * Whether this Policy is an `ALLOW` or `DENY`. --- * ``actions`` - [`[PolicyAction]!`]($gql:enum:PolicyAction) * The set of actions to allow or deny. --- * ``resources`` - [`[String]!`]($gql:scalar:String) * The resources to allow or deny. In the format `.` The following namespaces exist: - `financial` - `tenancy` - `public` - `system` As do the following resources in the financial namespace: - `Account` - `AccountSet` - `AccountSetMember` - `Transaction` - `Entry` - `Balance` - `TranCode` - `Journal` You can use `*` to wildcard as well. --- * ``assertions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * A map of expressions to evaluate this policy with. ## RestoreInput #### Input Fields --- * ``from_region`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The region in the current tenant to restore from. --- * ``to_region`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The region in the target `tenant` to restore to. --- * ``tenant`` - [`CreateTenantInput!`](/docs/reference/graphql/types/input#create-tenant-input) * The target tenant to create for restoration. --- * ``excludeTables`` - [`[ExcludeTableEnum]`]($gql:enum:ExcludeTableEnum) * List of tables to exclude from restoration. --- * ``exportTime`` - [`Timestamp`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp, up to 35 days in the past, to indicate from what time to export. --- * ``includeFileGlobs`` - [`[String]`]($gql:scalar:String) * List of glob patterns for files to include in restoration. By default no files are restored. Must supply patterns to restore files. Example: ["*.json"] includes all top level json files. ## SearchFilter SearchFilter supports Opensearch Query DSL under the "query" key See https://opensearch.org/docs/latest/query-dsl/ for more information on how to construct these queries. #### Input Fields --- * ``index`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `name` of the search index to use. --- * ``query`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * The Opensearch DSL query to use. --- * ``sort`` - [`[JSON]`]($gql:scalar:JSON) * The open search DSL sort to use. ## TempCredentials #### Input Fields --- * ``accessKeyId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``secretAccessKey`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``sessionToken`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## TranCodeEntryInput Defines the values for the entries written when transactions are posted with this tran code. #### Input Fields --- * ``accountId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Account ID for an entry written when this tran code is invoked. Expression must resolve to a UUID type. --- * ``units`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Units of currency for an entry written when this tran code is invoked. Expression must resolve to a Decimal type. --- * ``currency`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Currency used for an entry written when this tran code is invoked. Expression must resolve to a CurrencyCode type. --- * ``direction`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Direction for an entry written when this tran code is invoked. Expression must resolve to a DebitOrCredit enum type. --- * ``entryType`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Entry type for an entry written when this tran code is invoked. If omitted, defaults to `tranCode.code` with `_CR` or `_DR` appended depending on entry `direction`. Expression must resolve to a String type. --- * ``layer`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Layer for an entry written when this tran code is invoked. If omitted, defaults to `SETTLED` layer. Expression must resolve to a Layer enum type. --- * ``description`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Description for an entry written when this tran code is invoked." Expression must resolve to a String type. --- * ``metadata`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Metadata for the entry posted with this tran code. Expression must resolve to a JSON type. _Example:_ ``"{ 'x': 1, 'y': { 'z': 2 }}"`` --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression that indicates if this entry should be written. _Example:_ ``"params.amount > 0"`` ## TranCodeFilterInput Filter conditions to apply to a tran code query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``tranCodeId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `tranCodeId` field. Required when using index `TranCodeIndex.TRAN_CODE_ID`. --- * ``code`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `code` field. Only available when using index `TranCodeIndex.CODE`. --- * ``status`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `status` field. Only available when using index `TranCodeIndex.STATUS`. --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter conditions for a custom index. Only available when using index `TranCodeIndex.CUSTOM`. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Filter conditions for a search. Only available when using index `TranCodeIndex.SEARCH`. ## TranCodeIndexInput Specify the pre-defined TranCodeIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`TranCodeIndex!`](/docs/reference/graphql/types/enum#tran-code-index) * Indexes for querying TranCodes. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## TranCodeInput Fields to create a new TranCode. #### Input Fields --- * ``tranCodeId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Internal UUID for the transaction code record. --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The tran code represented as a unique string identifier. _Example:_ ``'ACH_CREDIT'`` --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Explanation of what this tran code represents and how it should be used. This provides documentation for the tran code. --- * ``params`` - [`[ParamDefinitionInput]`]($gql:input:ParamDefinitionInput) * Define the parameters that can be used when posting transactions using this tran code. --- * ``transaction`` - [`TranCodeTransactionInput!`](/docs/reference/graphql/types/input#tran-code-transaction-input) * Define the values for the transaction posted when this tran code is invoked. --- * ``entries`` - [`[TranCodeEntryInput!]!`]($gql:input:TranCodeEntryInput) * Define the values of entries written when transactions are posted with this tran code. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this tran code. --- * ``vars`` - [`ExpressionNestedMap`](/docs/reference/graphql/types/scalar#expression-nested-map) * Calculation area evaluated and injected as `vars` for transaction and entry evaluation. --- * ``assertions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Named boolean CEL expressions that must all evaluate to true when posting a transaction. Evaluated after params and vars are resolved. Failures return BadRequest. --- * ``workflow`` - [`TranCodeWorkflowInput`](/docs/reference/graphql/types/input#tran-code-workflow-input) * Workflow execution to trigger when transactions are posted with this tran code. ## TranCodeTransactionInput Define the values for the transaction posted when this tran code is invoked. #### Input Fields --- * ``effective`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Effective date for the transaction posted with this tran code. If ommitted, defaults to `date.Today()`. Expression must be a valid ISO 8601 formatted date. @example("date('2022-12-23')") --- * ``journalId`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Journal ID for the transaction posted with this tran code. If omitted, the default journal will be used. Expression must resolve to a UUID type. @example("uuid('b28f5684-0834-4292-8016-d2f2fb0367a9')") --- * ``correlationId`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Correlation ID for the transaction posted with this tran code. Expression must resolve to a String type. _Example:_ ``"'5a028997'"`` --- * ``externalId`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * External ID for the transaction posted with this tran code. Expression must resolve to a String type. _Example:_ ``"'45415819'"`` --- * ``description`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Description for the transaction posted with this tran code. Expression must resolve to a String type. @example("'TX for ' + string(params.amount)") --- * ``metadata`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Metadata for the transaction posted with this tran code. Expression must resolve to a JSON type. _Example:_ ``"{ 'x': 1, 'y': { 'z': 2 }}"`` ## TranCodeUpdateInput TranCode fields to update. #### Input Fields --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Explanation of what this tran code represents and how it should be used. This provides documentation for the tran code. --- * ``params`` - [`[ParamDefinitionInput]`]($gql:input:ParamDefinitionInput) * Define the parameters that can be used when posting transactions using this tran code. Replaces existing parameters definition. --- * ``transaction`` - [`TranCodeTransactionInput`](/docs/reference/graphql/types/input#tran-code-transaction-input) * Define values for transaction posted when this tran code is invoked. Replaces existing transaction definition. --- * ``entries`` - [`[TranCodeEntryInput]`]($gql:input:TranCodeEntryInput) * Define the values of entries written when transactions are posted with this tran code. Replaces existing entry definition. --- * ``status`` - [`Status`](/docs/reference/graphql/types/enum#status) * Operational status of the tran code. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this tran code. --- * ``vars`` - [`ExpressionNestedMap`](/docs/reference/graphql/types/scalar#expression-nested-map) * Variables for computation. --- * ``assertions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Named boolean CEL expressions that must all evaluate to true when posting a transaction. Evaluated after params and vars are resolved. Failures return BadRequest. --- * ``workflow`` - [`TranCodeWorkflowInput`](/docs/reference/graphql/types/input#tran-code-workflow-input) * Workflow execution to trigger when transactions are posted with this tran code. ## TranCodeWorkflowInput Input for workflow execution in tran code definition. #### Input Fields --- * ``workflowId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression for workflow ID. --- * ``executionId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression for execution ID. --- * ``task`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression for task name. --- * ``params`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * CEL expressions for workflow params. ## TransactionExceptionInput #### Input Fields --- * ``type`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable code that describes the type of exception. _Example:_ ``"FRAUD"`` --- * ``message`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable message that gives contextual detail about why this exception has occurred. _Example:_ ``"fraud system indicated 95% chance of fraud"`` --- * ``detail`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * Arbitrary structured data about this particular exception. ## TransactionFilterInput Filter conditions to apply to a transaction query. Filters are only applied if the field is used by the specified index. #### Input Fields --- * ``journalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Specify the Journal to use with `eq`. If omitted, the default journal will be used. --- * ``transactionId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `transactionId` field. Required when using index `TransactionIndex.TRANSACTION_ID`. --- * ``correlationId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `correlationId` field. Required when using index `TransactionIndex.CORRELATION_ID`. --- * ``externalId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `externalId` field. Required when using index `TransactionIndex.EXTERNAL_ID`. --- * ``group`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `group` field. Required when using index `TransactionIndex.GROUP`. --- * ``custom`` - [`CustomIndexFilter`](/docs/reference/graphql/types/input#custom-index-filter) * Filter conditions for a custom index. Only available when using index `TransactionIndex.CUSTOM`. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Filter conditions for a search. Only available when using index `TransactionIndex.SEARCH`. --- * ``transactionIds`` - [`[UUID]`]($gql:scalar:UUID) * Retrieve up to 100 transactions by id. ## TransactionIndexInput Specify the pre-defined TransactionIndex and sort order to use in a query. #### Input Fields --- * ``name`` - [`TransactionIndex!`](/docs/reference/graphql/types/enum#transaction-index) * Indexes for querying Transactions. To optimize query performance and apply desired filters, choose the appropriate index. --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## TransactionInput Fields to post a new Transaction. #### Input Fields --- * ``transactionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The ID is required to ensure an idempotent transaction. --- * ``tranCode`` - [`String!`](/docs/reference/graphql/types/scalar#string) * String corresponding to the `code` of a TranCode to be used for this transaction. --- * ``tranCodeVersion`` - [`Int`](/docs/reference/graphql/types/scalar#int) * Version of the tran code to use in this transaction. If not supplied, the latest version will be used. --- * ``params`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Params object specifying values for the params defined in the corresponding TranCode. --- * ``properties`` - [`TransactionPropertiesInput`](/docs/reference/graphql/types/input#transaction-properties-input) * Set various transaction properties, including changing velocity enforcement. ## TransactionPropertiesInput #### Input Fields --- * ``overrideVelocityEnforcement`` - [`VelocityEnforcementInput`](/docs/reference/graphql/types/input#velocity-enforcement-input) * Override velocity enforcement for this request. If a velocity control action will enforce at `VOID` or `REJECT`, will enforce with the action specified in this request. This is useful for force posts, where you want to disable a velocity control with an action of `WARN`. --- * ``exception`` - [`TransactionExceptionInput`](/docs/reference/graphql/types/input#transaction-exception-input) * If provided, will post transaction and immediately void. Will also create the transaction exception provided that is returned on `Transaction.exceptions`. Using this option will automatically set the `overrideVelocityEnforcement.action` to `WARN`, allowing all velocity controls to evaluate and write exceptions at WARN level. --- * ``idempotent`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Enables idempotent transaction posting. When true, if a transaction with this ID already exists, the existing transaction and its entries are returned instead of failing with a unique constraint violation. Guarantees: - Idempotency is keyed on `transactionId` — the caller must supply a stable ID. - The tran code must match: if the existing transaction was posted with a different tran code, the request fails with `BAD_REQUEST`. - The entries produced by the tran code must match: account, amount, direction, layer, and currency are compared. A mismatch fails with `BAD_REQUEST`. NOTE: During concurrent posting you may still receive a `TRANSACTION_ERROR` code; this is a standard retryable concurrency error. ## TransactionUpdateInput Transaction fields to update. #### Input Fields --- * ``externalId`` - [`String`](/docs/reference/graphql/types/scalar#string) * Allows specifying a unique external ID associated with this transaction. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of the transaction. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Arbitrary structured data about this transaction. ## UpdateClientInput #### Input Fields --- * ``policies`` - [`[PolicyInput]!`]($gql:input:PolicyInput) * Replaces the existing policies with this new set of policies. ## UpdateGroupInput #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A human-friendly name for the group, such as 'Admins' or 'DataAnalysts'. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * A brief description of the group's purpose, intended to provide additional context. --- * ``policy`` - [`String`](/docs/reference/graphql/types/scalar#string) * A set of policies to apply to this group, formatted as a JSON list that define the permissions granted to users within this group. Valid actions include `db:Insert`, `db:Update`, `db:Delete`, `db:Select` or wildcard `*` Example: ``` policy: "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"],\"assertions\": {\"always false\": \"1 == 0\"}}]" ``` ## UpdateLimitInput #### Input Fields --- * ``timestampSource`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Uses timestamp from the specified source for picking the balance limit. By default uses the transaction timestamp. Must resolve to a CEL `timestamp`. @example("timestamp(context.vars.transaction.?metadata.ts.orValue(context.transaction.timestamp))") --- * ``balance`` - [`[BalanceLimitInput]`]($gql:input:BalanceLimitInput) * ## UpdateTenantInput #### Input Fields --- * ``accountId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A globally unique identifier representing an environment within the organization. This accountId, when combined with an AWS region, is used to calculate the database tenant. --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * A human-friendly name for the tenant, used for display purposes and easier identification. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * A brief description of the tenant, providing additional context about its purpose or characteristics. ## UpdateUserInput #### Input Fields --- * ``groupIds`` - [`[UUID]`]($gql:scalar:UUID) * A list of unique identifiers for the groups to which the user belongs. The user's permissions are determined by the combined policies of these groups. --- * ``email`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The user's email address, which serves as a unique identifier and primary means of contact. ## UpdateVelocityControlInput #### Input Fields --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * If set, updates name of velocity control. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * If set, updates description of velocity control. --- * ``enforcement`` - [`VelocityEnforcementInput`](/docs/reference/graphql/types/input#velocity-enforcement-input) * If set, updates the enforcement type of the velocity control. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * If set, updates the condition for the velocity control. ## UpdateVelocityLimitInput #### Input Fields --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * If set, updates name of velocity limit. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * If set, updates description of velocity limit. --- * ``limit`` - [`UpdateLimitInput`](/docs/reference/graphql/types/input#update-limit-input) * If set, updates the limit of the velocity limit. --- * ``currency`` - [`CurrencyCode`](/docs/reference/graphql/types/scalar#currency-code) * If set, sets the currency of the velocity limit. Cannot unset. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * If Set, sets the condition of the velocity limit. ## UsageInput #### Input Fields --- * ``start`` - [`Date!`](/docs/reference/graphql/types/scalar#date) * Start date at midnight UTC. --- * ``end`` - [`Date!`](/docs/reference/graphql/types/scalar#date) * End date at midnight UTC. ## VelocityControlFilterInput #### Input Fields --- * ``velocityControlId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `velocityControlId` field. Required when using index `VelocityControlIndex.VELOCITY_CONTROL_ID`. --- * ``velocityLimitId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `velocityLimitId` field. Required when using index `VelocityControlIndex.VELOCITY_RULE_ID`. --- * ``name`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `name` field. Required when using index `VelocityControlIndex.NAME`. --- * ``accountId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on attached accounts via `accountId`. Required when using index `VelocityControlIndex.ACCOUNT_ID`. ## VelocityControlIndexInput #### Input Fields --- * ``name`` - [`VelocityControlIndex!`](/docs/reference/graphql/types/enum#velocity-control-index) * --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## VelocityControlInput #### Input Fields --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this velocity control. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name for this velocity control. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable description for this velocity control. --- * ``enforcement`` - [`VelocityEnforcementInput!`](/docs/reference/graphql/types/input#velocity-enforcement-input) * The type of enforcement this velocity control generates. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if this control should trigger enforcement. The `account`, `transaction` and `entry` are available for use on `context.vars`. @example("context.vars.transaction.?metadata.skipVelocityControl.orElse(false))") --- * ``velocityLimitIds`` - [`[UUID]`]($gql:scalar:UUID) * Add these velocity limits to the control. ## VelocityEnforcementInput #### Input Fields --- * ``action`` - [`VelocityEnforcementAction!`](/docs/reference/graphql/types/enum#velocity-enforcement-action) * ## VelocityLimitFilterInput #### Input Fields --- * ``velocityLimitId`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `velocityLimitId` field. Required when using index `VelocityLimitIndex.VELOCITY_RULE_ID`. --- * ``name`` - [`FilterValue`](/docs/reference/graphql/types/input#filter-value) * Filter on the `name` field. Required when using index `VelocityLimitIndex.NAME`. ## VelocityLimitIndexInput #### Input Fields --- * ``name`` - [`VelocityLimitIndex!`](/docs/reference/graphql/types/enum#velocity-limit-index) * --- * ``sort`` - [`SortOrder`](/docs/reference/graphql/types/enum#sort-order) * `ASC` (ascending) or `DESC` (descending). ## VelocityLimitInput #### Input Fields --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name of this rule. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * " Human readable description of this rule. --- * ``window`` - [`[PartitionKeyInput]!`]($gql:input:PartitionKeyInput) * Group by these values to index the calculation. The `account`, `transaction`, `tranCode` and `entry` are available for use in the window computation on `context.vars`. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if an balance entry should be written. The `account`, `transaction` and `entry` are available for use in the window computation on `context.vars`. @example("has(context.vars.account.metadata.policyPayment)") --- * ``limit`` - [`LimitInput!`](/docs/reference/graphql/types/input#limit-input) * The limit to enforce. Can supply different limits based --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * Currency this limit applies to. If set to empty string, applies limit to all currencies. --- * ``params`` - [`[ParamDefinitionInput]`]($gql:input:ParamDefinitionInput) * The parameters for `VelocityLimit.limit`. --- * ``velocityControlIds`` - [`[UUID]`]($gql:scalar:UUID) * Add the limit to the velocity controls in this list. ## VelocityWindowInput #### Input Fields --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The account or set id to search for velocity. --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Limit the search to this velocity control Id. --- * ``velocityLimitId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Limit the search to this velocity limit. --- * ``window`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * The window to search. If `velocityLimitId` not present, will return any limit that supports the _entire_ window. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * Return the velocity limit for this currency. _Default:_ ``"USD"`` ## ViewConfigInput #### Input Fields --- * ``enableConcurrentPosting`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. _Default:_ ``true`` ## ViewFilter Filter input for querying view entries. #### Input Fields --- * ``index`` - [`ViewIndexFilter`](/docs/reference/graphql/types/input#view-index-filter) * Use the index filter for standard index queries. --- * ``search`` - [`SearchFilter`](/docs/reference/graphql/types/input#search-filter) * Use the search filter for search index queries. ## ViewIndexFilter #### Input Fields --- * ``index`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name of the index to use. If not provided, the standard index is queried. --- * ``partition`` - [`[CustomIndexFilterValue]`]($gql:input:CustomIndexFilterValue) * Specify the partition of view index. --- * ``sort`` - [`[CustomIndexFilterValue]`]($gql:input:CustomIndexFilterValue) * Speficy the sort of the view index. ## ViewIndexInput #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``unique`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is unique. --- * ``partition`` - [`[PartitionKeyInput]!`]($gql:input:PartitionKeyInput) * The partition key used for this index. --- * ``partitionShardCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * Specifies the number of shards for partition write scaling. This parameter defines how many shards the partition key is automatically split into, similarly to RAID-style disk striping. Increasing this value allows the index to distribute write throughput across multiple shards while sacrificing global sort order on the partition. For instance, setting `partitionShardCount` to 4 splits each unique partition into four shards, effectively allowing 4000 writes per second for a single partition key. --- * ``sort`` - [`[IndexKeyInput]!`]($gql:input:IndexKeyInput) * The sort key to use for supporting range queries. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCategory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. ## ViewSearchIndexInput #### Input Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCategory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. --- * ``opensearchSchema`` - [`OpensearchSchemaInput!`](/docs/reference/graphql/types/input#opensearch-schema-input) * Opensearch mapping (with CEL expressions) applied to the document prior to indexing. Required. Every view search index must declare its field types explicitly so that sort, filter, and aggregation behavior is stable across index creations. ## ViewSourceInput #### Input Fields --- * ``entity`` - [`ViewEntity!`](/docs/reference/graphql/types/enum#view-entity) * Enum of source tables that can trigger view updates. --- * ``triggers`` - [`[ViewTriggerEnum!]!`]($gql:enum:ViewTriggerEnum) * ## VoidTransactionPropertiesInput #### Input Fields --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``idempotent`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When true, makes the void operation idempotent. If the target transaction does not exist, returns null. If the target transaction is already voided, returns the existing void transaction. ## WorkflowInput Fields to execute a new workflow. #### Input Fields --- * ``workflowId`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``task`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``params`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- # Interface Types Interface types define a set of fields that other object types can implement, ensuring consistency in the schema. Source: https://www.twisp.com/docs/reference/graphql/types/interface ## Connection Connection types must contain a `pageInfo` field as well as `nodes` and `edges`. #### Fields --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## Node The generic Node interface. All first-class entities in the API implement this interface. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. See: https://graphql.org/learn/global-object-identification/ --- # Object Types Object types represent complex objects with multiple fields and can be queried for specific data. Source: https://www.twisp.com/docs/reference/graphql/types/object ## AbortedSummary #### Fields --- * ``count`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of aborted requests. --- * ``errors`` - [`[FailedBulkExecutionError]`]($gql:object:FailedBulkExecutionError) * Errors from the FAILED_N.json files. ## Account Accounts model all of the economic activity that your ledger provides. The chart of accounts is the basis for creating balance sheets, P&L reports, and for understanding the balances for the customer and business entities your business services. Accounts can be organized into sets with the AccountSet type. Hierarchical tree structures which roll up balances across many accounts can be modeled by nesting sets within other sets. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `ac:` where `key` is `base64(json({ 1: accountId }))`. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the account. --- * ``externalId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Allows specifying a unique external ID associated with this account. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Account name. _Examples:_ ``"Bill Pay Settlement"``, ``"Courtesy Credit"`` --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Shorthand code for the account, often an abbreviated version of the account name. Example: 'ACH_RECON' for an account named 'ACH Reconciliation'. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of the account. --- * ``status`` - [`AccountStatus!`](/docs/reference/graphql/types/enum#account-status) * Current status for the account. --- * ``normalBalanceType`` - [`DebitOrCredit!`](/docs/reference/graphql/types/enum#debit-or-credit) * Flag indicating whether this account uses a "debit normal" or a "credit normal" balance. In double-entry accounting, accounts with a debit normal balance use the balance calculation `balance = debits - credits`. This is used for asset and expense account types. Accounts with a credit normal balance, in contrast, calculate their balance with the equation `balance = credits - debits`. This is the default type for liabilities, equity, and revenue account types. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this account. --- * ``config`` - [`AccountConfig!`](/docs/reference/graphql/types/object#account-config) * System config for this account. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the account was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this account. Previous versions are tracked in `history`. --- * ``balances`` - [`BalanceConnection!`](/docs/reference/graphql/types/object#balance-connection) * Reference to the balances for this account. Accounts have balances across all three layers: SETTLED, PENDING, and ENCUMBRANCE. Each balance reflects the current total debits and credits for all entries in this account within the specified journal and currency. --- * ``balance`` - [`Balance`](/docs/reference/graphql/types/object#balance) * Reference to the balance for a specific journal and currency (defaults to "USD"). --- * ``entries`` - [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) * All ledger entries associated with this account. --- * ``sets`` - [`AccountSetConnection!`](/docs/reference/graphql/types/object#account-set-connection) * Accounts can be organized into sets. Each account can belong to zero or multiple account sets. --- * ``history`` - [`AccountConnection!`](/docs/reference/graphql/types/object#account-connection) * History of changes to this Account record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. --- * ``velocity`` - [`[VelocityBalance]`]($gql:object:VelocityBalance) * --- * ``controls`` - [`ResolvedVelocityControlConnection!`](/docs/reference/graphql/types/object#resolved-velocity-control-connection) * Set of controls attached to this account. --- * ``calculations`` - [`[Calculation!]!`]($gql:object:Calculation) * Retrieve the calculations that are explicitly attached to this account. ## AccountConfig System configuration for an account. #### Fields --- * ``enableConcurrentPosting`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, allow concurrent posting to the account. See `BalanceType` for balance retrieval options available for concurrent-enabled accounts. Defaults to `false`. --- * ``isAccountSet`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, indicates this account is an underlying account for an account set. ## AccountConnection Connection to a list of Account nodes. Access Account nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Account]!`]($gql:object:Account) * Accounts model all of the economic activity that your ledger provides. The chart of accounts is the basis for creating balance sheets, P&L reports, and for understanding the balances for the customer and business entities your business services. Accounts can be organized into sets with the AccountSet type. Hierarchical tree structures which roll up balances across many accounts can be modeled by nesting sets within other sets. --- * ``edges`` - [`[AccountConnectionEdge]!`]($gql:object:AccountConnectionEdge) * Edges represent links connecting a parent or query field to a list of Account nodes. They contain a reference to the Account node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## AccountConnectionEdge Edges represent links connecting a parent or query field to a list of Account nodes. They contain a reference to the Account node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Account`](/docs/reference/graphql/types/object#account) * Reference to the Account node at this edge. ## AccountSet A set of accounts. Account sets contain _members_ which can include accounts as well as other account sets. Every account set has multiple _balances_ which represent the sum of all balances of member accounts and member account sets. Like balances for accounts, account set balances are computed for every currency used by the entries posted to accounts in a set and all of its sub-sets. Because account sets are tied to a specific journal, they only compute balances using entries posted to their journal. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `as:` where `key` is `base64(json({ 1: accountSetId }))`. --- * ``accountSetId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the set. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The journal for the set. Account sets are confined to a single journal and roll up balances for entries on their journal. Account sets can only contain other sets using the same journal. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name for the set. --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * code for the account set. Unique value. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of the account set. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this account set. --- * ``config`` - [`AccountSetConfig!`](/docs/reference/graphql/types/object#account-set-config) * System config for this account set. --- * ``normalBalanceType`` - [`DebitOrCredit!`](/docs/reference/graphql/types/enum#debit-or-credit) * Indicates whether this account set uses a "debit normal" or a "credit normal" balance. In double-entry accounting, a debit normal balance uses the calculation `balance = debits - credits`. A credit normal balance, in contrast, is calculated with the equation `balance = credits - debits`. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the account set was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this account set. Previous versions are tracked in `history`. --- * ``balance`` - [`Balance`](/docs/reference/graphql/types/object#balance) * Reference to the balance for a specific currency (defaults to "USD"). --- * ``balances`` - [`BalanceConnection!`](/docs/reference/graphql/types/object#balance-connection) * Reference to the balances for this account set. Each balance reflects the current sum of debits and credits for all entries on accounts in this set and all accounts in any sub-sets, on the current layer and all layers above. Because account sets are tied to a specific journal, they only compute balances using entries posted to their journal. --- * ``entries`` - [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) * All ledger entries associated with accounts in this set and in all subsets. --- * ``history`` - [`AccountSetConnection!`](/docs/reference/graphql/types/object#account-set-connection) * History of changes to this AccountSet record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. --- * ``members`` - [`AccountSetMemberConnection!`](/docs/reference/graphql/types/object#account-set-member-connection) * Eventually consistent list of all members of the account set. Sets can include other account sets. When a `memberId` `eq` filter is supplied, a strongly consistent index is used. --- * ``sets`` - [`AccountSetConnection!`](/docs/reference/graphql/types/object#account-set-connection) * Account sets can be organized into sets. Each account set can belong to zero or multiple account sets. --- * ``velocity`` - [`[VelocityBalance]`]($gql:object:VelocityBalance) * Check the velocity balance for this account set. --- * ``controls`` - [`ResolvedVelocityControlConnection!`](/docs/reference/graphql/types/object#resolved-velocity-control-connection) * Set of controls attached to this account set. --- * ``calculations`` - [`[Calculation!]!`]($gql:object:Calculation) * Retrieve the calculations that are explicitly attached to this account set. ## AccountSetConfig System configuration for an account set. #### Fields --- * ``enableConcurrentPosting`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * When `true`, allow concurrent posting to the account. See `BalanceType` for balance retrieval options available for concurrent-enabled accounts. Defaults to `false`. ## AccountSetConnection Connection to a list of AccountSet nodes. Access AccountSet nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[AccountSet]!`]($gql:object:AccountSet) * A set of accounts. Account sets contain _members_ which can include accounts as well as other account sets. Every account set has multiple _balances_ which represent the sum of all balances of member accounts and member account sets. Like balances for accounts, account set balances are computed for every currency used by the entries posted to accounts in a set and all of its sub-sets. Because account sets are tied to a specific journal, they only compute balances using entries posted to their journal. --- * ``edges`` - [`[AccountSetConnectionEdge]!`]($gql:object:AccountSetConnectionEdge) * Edges represent links connecting a parent or query field to a list of AccountSet nodes. They contain a reference to the AccountSet node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## AccountSetConnectionEdge Edges represent links connecting a parent or query field to a list of AccountSet nodes. They contain a reference to the AccountSet node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`AccountSet`](/docs/reference/graphql/types/object#account-set) * Reference to the AccountSet node at this edge. ## AccountSetMemberConnection Connection to a list of AccountSetMember nodes. Access AccountSetMember nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[AccountSetMember!]!`]($gql:union:AccountSetMember) * Account set members can be of type Account or AccountSet. --- * ``edges`` - [`[AccountSetMemberConnectionEdge]!`]($gql:object:AccountSetMemberConnectionEdge) * Edges represent links connecting a parent or query field to a list of AccountSetMember nodes. They contain a reference to the AccountSetMember node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## AccountSetMemberConnectionEdge Edges represent links connecting a parent or query field to a list of AccountSetMember nodes. They contain a reference to the AccountSetMember node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`AccountSetMember`](/docs/reference/graphql/types/union#account-set-member) * Reference to the AccountSetMember node at this edge. ## AchAdvBatchControl #### Fields --- * ``serviceClassCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryAddendaCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryHash`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalDebitEntryDollarAmount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalCreditEntryDollarAmount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``achOperatorData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``odfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``batchNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchAdvEntryDetail #### Fields --- * ``transactionCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``rdfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``checkDigit`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dfiAccountNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``amount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``adviceRoutingNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``fileIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``achOperatorData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``individualName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``discretionaryData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addendaRecordIndicator`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``achOperatorRoutingNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``julianDay`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``sequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``addenda99`` - [`Addenda99`](/docs/reference/graphql/types/object#addenda99) * ## AchAdvFileControl #### Fields --- * ``batchCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``blockCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryAddendaCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryHash`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalDebitEntryDollarAmountInFile`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalCreditEntryDollarAmountInFile`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchBatch #### Fields --- * ``header`` - [`AchBatchHeader`](/docs/reference/graphql/types/object#ach-batch-header) * --- * ``control`` - [`AchBatchControl`](/docs/reference/graphql/types/object#ach-batch-control) * --- * ``advControl`` - [`AchAdvBatchControl`](/docs/reference/graphql/types/object#ach-adv-batch-control) * --- * ``offset`` - [`AchOffset`](/docs/reference/graphql/types/object#ach-offset) * ## AchBatchControl #### Fields --- * ``serviceClassCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryAddendaCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryHash`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalDebitEntryDollarAmount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalCreditEntryDollarAmount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``companyIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``messageAuthenticationCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``odfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``batchNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchBatchHeader #### Fields --- * ``serviceClassCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``companyName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``companyDiscretionaryData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``companyIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``standardEntryClassCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``companyEntryDescription`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``companyDescriptiveDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``effectiveEntryDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``settlementDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorStatusCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``odfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``batchNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchConfiguration #### Fields --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this configuration. --- * ``endpointId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Endpoint to use for decisioning this ACH file. --- * ``settlementAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ACH Settlement Account. --- * ``exceptionAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account to post to when an exception occurs, such as a Velocity Control or Account in a locked state. Funds in this account will be returned. --- * ``suspenseAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Account to post to when an account is not found. Funds in this account will be returned. --- * ``feeAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ACH Fee Account. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Journal to post settlements into. --- * ``odfiHeaderConfiguration`` - [`AchOdfiHeaderConfiguration!`](/docs/reference/graphql/types/object#ach-odfi-header-configuration) * ACH Processor immediate destination/origin configuration. --- * ``timeZone`` - [`String!`](/docs/reference/graphql/types/scalar#string) * IANA Timezone identifier for the configuration. _Example:_ ``"America/Chicago"`` --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchConfigurationConnection #### Fields --- * ``nodes`` - [`[AchConfiguration]!`]($gql:object:AchConfiguration) * --- * ``edges`` - [`[AchConfigurationConnectionEdge]!`]($gql:object:AchConfigurationConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## AchConfigurationConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`AchConfiguration`](/docs/reference/graphql/types/object#ach-configuration) * ## AchEntryDetail #### Fields --- * ``transactionCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``rdfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``checkDigit`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dfiAccountNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``amount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``identificationNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``individualName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``discretionaryData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addendaRecordIndicator`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addenda02`` - [`Addenda02`](/docs/reference/graphql/types/object#addenda02) * --- * ``addenda05`` - [`[Addenda05]`]($gql:object:Addenda05) * --- * ``addenda98`` - [`Addenda98`](/docs/reference/graphql/types/object#addenda98) * --- * ``addenda98Refused`` - [`Addenda98Refused`](/docs/reference/graphql/types/object#addenda98refused) * --- * ``addenda99`` - [`Addenda99`](/docs/reference/graphql/types/object#addenda99) * --- * ``addenda99Contested`` - [`Addenda99Contested`](/docs/reference/graphql/types/object#addenda99contested) * --- * ``addenda99Dishonored`` - [`Addenda99Dishonored`](/docs/reference/graphql/types/object#addenda99dishonored) * ## AchFile #### Fields --- * ``header`` - [`AchFileHeader!`](/docs/reference/graphql/types/object#ach-file-header) * --- * ``control`` - [`AchFileControl`](/docs/reference/graphql/types/object#ach-file-control) * --- * ``advControl`` - [`AchAdvFileControl`](/docs/reference/graphql/types/object#ach-adv-file-control) * ## AchFileControl #### Fields --- * ``batchCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``blockCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryAddendaCount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryHash`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalDebitEntryDollarAmountInFile`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``totalCreditEntryDollarAmountInFile`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchFileHeader #### Fields --- * ``priorityCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateDestination`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateOrigin`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``fileCreationDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``fileCreationTime`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``fileIdModifier`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``recordSize`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``blockingFactor`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``formatCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateDestinationName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateOriginName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``referenceCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## AchFileInfo #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `afi:` where `key` is `base64(json({ 1: fileId }))`. --- * ``fileId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier derived from fileKey to indentify a particular file being processed. --- * ``fileKey`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The file key being processed. --- * ``fileVersion`` - [`String!`](/docs/reference/graphql/types/scalar#string) * TBD --- * ``fileDate`` - [`String!`](/docs/reference/graphql/types/scalar#string) * File date from header of ACH file. --- * ``fileModifier`` - [`String!`](/docs/reference/graphql/types/scalar#string) * File modifier from header of ACH file. --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The ACH configuration id used to process this file. --- * ``configVersion`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The version number of the configuration used to process this file. --- * ``fileType`` - [`AchFileType!`](/docs/reference/graphql/types/enum#ach-file-type) * The type of file being processed. --- * ``processingStatus`` - [`AchFileProcessingStatus!`](/docs/reference/graphql/types/enum#ach-file-processing-status) * The last reported processing status of this file. --- * ``processingDetail`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The last string message of the processing detail of this file. --- * ``processingStatistics`` - [`AchProcessingStatistics!`](/docs/reference/graphql/types/object#ach-processing-statistics) * Stats about the items in process for this file. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this file process. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of when this file info was created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of the last modification to this file info. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this `FileInfo`. Previous versions are tracked in `history`. --- * ``history`` - [`AchFileInfoConnection!`](/docs/reference/graphql/types/object#ach-file-info-connection) * History of changes to this ACH file info. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. --- * ``records`` - [`FileRecordConnection!`](/docs/reference/graphql/types/object#file-record-connection) * Returns the records associated with this file. ## AchFileInfoConnection Connection to a list of ACH file nodes. Access Account nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[AchFileInfo]!`]($gql:object:AchFileInfo) * --- * ``edges`` - [`[AchFileInfoConnectionEdge]!`]($gql:object:AchFileInfoConnectionEdge) * Edges represent links connecting a parent or query field to a list of ACH file nodes. They contain a reference to the ACH file node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## AchFileInfoConnectionEdge Edges represent links connecting a parent or query field to a list of ACH file nodes. They contain a reference to the ACH file node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`AchFileInfo`](/docs/reference/graphql/types/object#ach-file-info) * Reference to the Account node at this edge. ## AchGeneratedFile #### Fields --- * ``fileKey`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The key where file is stored, if generated. --- * ``generated`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Indicates a file was generated or not. ## AchIatBatch #### Fields --- * ``header`` - [`AchIatBatchHeader`](/docs/reference/graphql/types/object#ach-iat-batch-header) * --- * ``control`` - [`AchBatchControl`](/docs/reference/graphql/types/object#ach-batch-control) * ## AchIatBatchHeader #### Fields --- * ``serviceClassCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``iatIndicator`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignExchangeIndicator`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignExchangeReferenceIndicator`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``foreignExchangeReference`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``isoDestinationCountryCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``standardEntryClassCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``companyEntryDescription`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``isoOriginatingCurrencyCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``isoDestinationCurrencyCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``effectiveEntryDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``settlementDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorStatusCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``odfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``batchNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## AchIatEntryDetail #### Fields --- * ``transactionCode`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``rdfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``checkDigit`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addendaRecords`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``amount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``dfiAccountNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``ofacScreeningIndicator`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``secondaryOfacScreeningIndicator`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addendaRecordIndicator`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addenda10`` - [`Addenda10`](/docs/reference/graphql/types/object#addenda10) * --- * ``addenda11`` - [`Addenda11`](/docs/reference/graphql/types/object#addenda11) * --- * ``addenda12`` - [`Addenda12`](/docs/reference/graphql/types/object#addenda12) * --- * ``addenda13`` - [`Addenda13`](/docs/reference/graphql/types/object#addenda13) * --- * ``addenda14`` - [`Addenda14`](/docs/reference/graphql/types/object#addenda14) * --- * ``addenda15`` - [`Addenda15`](/docs/reference/graphql/types/object#addenda15) * --- * ``addenda16`` - [`Addenda16`](/docs/reference/graphql/types/object#addenda16) * --- * ``addenda17`` - [`[Addenda17]`]($gql:object:Addenda17) * --- * ``addenda18`` - [`[Addenda18]`]($gql:object:Addenda18) * --- * ``addenda98`` - [`Addenda98`](/docs/reference/graphql/types/object#addenda98) * --- * ``addenda99`` - [`Addenda99`](/docs/reference/graphql/types/object#addenda99) * ## AchOdfiHeaderConfiguration #### Fields --- * ``immediateDestination`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateDestinationName`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateOrigin`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``immediateOriginName`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## AchOffset #### Fields --- * ``routingNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``accountNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``accountType`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## AchProcessedFile #### Fields --- * ``fileId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ## AchProcessingStatistics #### Fields --- * ``numEntriesUnprocessed`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The number of entries remaining that need to respond to create webhook. --- * ``totalCreditAmount`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Total credit amount in the file. --- * ``totalDebitAmount`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Total debit amount in the file. ## AchWorkflowTrace Audit trail that links workflow executions to specific ACH entries. Provides complete traceability from business logic execution to ACH network transactions enabling returns processing, reconciliation, and compliance auditing. #### Fields --- * ``workflowId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the workflow template that processed this ACH entry. Links to the business logic used for processing this transaction. --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the specific execution instance of the workflow. Each workflow run gets a unique execution ID for audit and debugging purposes. --- * ``fileId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the ACH file containing this entry. References the source file that was processed. --- * ``recordId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the specific ACH entry detail record. Links to the individual record within the ACH file. --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the ACH configuration used to process this entry. References the processing rules, accounts, and ODFI settings applied. --- * ``traceNumber`` - [`String!`](/docs/reference/graphql/types/scalar#string) * ACH trace number from the entry detail record (15 digits). Required for ACH returns, corrections, and reconciliation with the banking network. Format: ODFI routing number (8 digits) + sequence number (7 digits). _Example:_ ``"123456780001234"`` ## Addenda02 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``referenceInformationOne`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``referenceInformationTwo`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``terminalIdentificationCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``transactionSerialNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``transactionDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``authorizationCodeOrExpireDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``terminalLocation`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``terminalCity`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``terminalState`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Addenda05 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``paymentRelatedInformation`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``sequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda10 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``transactionTypeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignPaymentAmount`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``foreignTraceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda11 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorStreetAddress`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda12 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorCityStateProvince`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originatorCountryPostalCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda13 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``odfiName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``odfiIdNumberQualifier`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``odfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``odfiBranchCountryCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda14 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``rdfiName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``rdfiIdNumberQualifier`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``rdfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``rdfiBranchCountryCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda15 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``receiverIdNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``receiverStreetAddress`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda16 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``receiverCityStateProvince`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``receiverCountryPostalCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda17 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``paymentRelatedInformation`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``sequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda18 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignCorrespondentBankName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignCorrespondentBankIdNumberQualifier`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignCorrespondentBankIdNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``foreignCorrespondentBankBranchCountryCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``sequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryDetailSequenceNumber`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Addenda98 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``changeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalTrace`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalDfi`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``correctedData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``iatCorrectedData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Addenda98Refused #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``refusedChangeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalTrace`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalDfi`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``correctedData`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``changeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceSequenceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Addenda99 #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalTrace`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dateOfDeath`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalDfi`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addendaInformation`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Addenda99Contested #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``contestedReturnCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalEntryTraceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dateOriginalEntryReturned`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalReceivingDfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalSettlementDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnTraceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnSettlementDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnReasonCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dishonoredReturnTraceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dishonoredReturnSettlementDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dishonoredReturnReasonCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Addenda99Dishonored #### Fields --- * ``typeCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dishonoredReturnReasonCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalEntryTraceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``originalReceivingDfiIdentification`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnTraceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnSettlementDate`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``returnReasonCode`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``addendaInformation`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``traceNumber`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## AttachedCalculation #### Fields --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``account`` - [`Account!`](/docs/reference/graphql/types/object#account) * Accounts model all of the economic activity that your ledger provides. The chart of accounts is the basis for creating balance sheets, P&L reports, and for understanding the balances for the customer and business entities your business services. Accounts can be organized into sets with the AccountSet type. Hierarchical tree structures which roll up balances across many accounts can be modeled by nesting sets within other sets. --- * ``calculation`` - [`Calculation!`](/docs/reference/graphql/types/object#calculation) * ## Balance Balances are auto-calculated sums of the entries for a given account. Every balance record maintains a `drBalance` for entries on the debit side of the ledger and a `crBalance` for credit entries. Additionally, every account has a `normalBalance`, which is equal to `crBalance - drBalance` for credit normal accounts, and `drBalance - crBalance` for debit normal accounts. Each account can have balances across all three layers: SETTLED, PENDING, and ENCUMBRANCE. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `ba:` where `key` is `base64(json({ 1: accountId, 2: journalId, 3: currency, 4: calculationId, 5: dimension }))` The journalId and currency are optional and will use the default values if they are missing. The calculationId and dimension are included for calculation-specific balances. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the journal within which the balance is calculated. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the account for which the balance is calculated. --- * ``entryId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the most recent entry used to calculate the balance. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * The currency of the balance amounts. Balances represent the sum of entries using the same currency. Multi-currency ledgers will therefore have different balances for each currency. --- * ``settled`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts on the settled layer. --- * ``pending`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts on the pending layer. --- * ``encumbrance`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts on the encumbrance layer. --- * ``dimensions`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * The dimensions that make up this balance calculation --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The calculationId of this balance --- * ``available`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts available by combining the provided layer with all layers above. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the balance was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this balance. Previous versions are tracked in `history`. --- * ``effectiveBalances`` - [`[EffectivePeriod]`]($gql:object:EffectivePeriod) * Per-period entries when queried with `effective.cumulative`, `effective.range`, or `effective.periods`. Each entry's `effective` field identifies the period it covers. Contents depend on the `Effective` variant used: - `cumulative`: the underlying year/month/day buckets that the aggregate was summed from. - `range`: one entry per bucket in `[gte, lte]`, each holding that period's activity. - `periods`: one entry per period in the half-open `[gte, lt)` interval in chronological order. With `accumulate: true`, each entry is the cumulative balance through the end of that period; with `accumulate: false` (default), each entry is that period's own activity. --- * ``account`` - [`Account!`](/docs/reference/graphql/types/object#account) * Reference to the balance's account. --- * ``entry`` - [`Entry!`](/docs/reference/graphql/types/object#entry) * Reference to the most recent entry used to calculate the balance. --- * ``entries`` - [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) * All ledger entries for this balance. --- * ``journal`` - [`Journal!`](/docs/reference/graphql/types/object#journal) * Reference to the balance's journal. --- * ``history`` - [`BalanceConnection!`](/docs/reference/graphql/types/object#balance-connection) * History of changes to this Balance record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. ## BalanceAmount #### Fields --- * ``drBalance`` - [`Money!`](/docs/reference/graphql/types/object#money) * Sum of all amounts for entries on the DEBIT side of the ledger. --- * ``crBalance`` - [`Money!`](/docs/reference/graphql/types/object#money) * Sum of all amounts for entries on the CREDIT side of the ledger. --- * ``normalBalance`` - [`Money!`](/docs/reference/graphql/types/object#money) * The "normal balance" for an account is different for credit normal and debit normal accounts. For credit normal accounts, the normal balance is equal to `crBalance - drBalance`. For debit normal accounts, the normal balance is the reverse: `drBalance - crBalance`. --- * ``entryId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the most recent entry used to calculate the balance on this layer. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change of balance on this layer. ## BalanceConnection Connection to a list of Balance nodes. Access Balance nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Balance]!`]($gql:object:Balance) * Balances are auto-calculated sums of the entries for a given account. Every balance record maintains a `drBalance` for entries on the debit side of the ledger and a `crBalance` for credit entries. Additionally, every account has a `normalBalance`, which is equal to `crBalance - drBalance` for credit normal accounts, and `drBalance - crBalance` for debit normal accounts. Each account can have balances across all three layers: SETTLED, PENDING, and ENCUMBRANCE. --- * ``edges`` - [`[BalanceConnectionEdge]!`]($gql:object:BalanceConnectionEdge) * Edges represent links connecting a parent or query field to a list of Balance nodes. They contain a reference to the Balance node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## BalanceConnectionEdge Edges represent links connecting a parent or query field to a list of Balance nodes. They contain a reference to the Balance node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Balance`](/docs/reference/graphql/types/object#balance) * Reference to the Balance node at this edge. ## BalanceLimit #### Fields --- * ``layer`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The layer this balance limit is enforced at. --- * ``amount`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The maximum amount at this layer that can be spent. --- * ``NormalBalanceType`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The direction this balance enforces on. --- * ``start`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * The timestamp at which this balance limit begins to be effective. --- * ``end`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * The timestamp at which this balance limit ceases to be effective. ## BatchExecuteStatementOutput #### Fields --- * ``createdAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``database`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dbUser`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``id`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## BulkQueryExecution #### Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name of this bulk execution. --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Key to variables file to use. --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique Identifier for this execution of a bulk query. --- * ``query`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Parameterized GraphQL query string to execute. --- * ``status`` - [`BulkQueryExecutionStatus!`](/docs/reference/graphql/types/enum#bulk-query-execution-status) * Status of this execution. --- * ``resultKeys`` - [`[String]`]($gql:scalar:String) * Set when `status` is `COMPLETE`, lists the keys to download the results files. --- * ``error`` - [`String`](/docs/reference/graphql/types/scalar#string) * If `status` in error, a diagnostic message for the cause of the error. --- * ``summary`` - [`BulkQueryExecutionSummary`](/docs/reference/graphql/types/object#bulk-query-execution-summary) * Summary of the bulk query execution. Populated on COMPLETE or ERROR. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. ## BulkQueryExecutionConnection Connection to a list of BulkQueryExecution nodes. Access BulkQueryExecution nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[BulkQueryExecution]!`]($gql:object:BulkQueryExecution) * --- * ``edges`` - [`[BulkQueryExecutionConnectionEdge]!`]($gql:object:BulkQueryExecutionConnectionEdge) * Edges represent links connecting a parent or query field to a list of BulkQueryExecution nodes. They contain a reference to the BulkQueryExecution node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## BulkQueryExecutionConnectionEdge Edges represent links connecting a parent or query field to a list of BulkQueryExecution nodes. They contain a reference to the BulkQueryExecution node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`BulkQueryExecution`](/docs/reference/graphql/types/object#bulk-query-execution) * Reference to the BulkQueryExecution node at this edge. ## BulkQueryExecutionSummary #### Fields --- * ``total`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * total number of requests in this execution. --- * ``executed`` - [`ExecutedSummary!`](/docs/reference/graphql/types/object#executed-summary) * Summary of executed requests. --- * ``aborted`` - [`AbortedSummary!`](/docs/reference/graphql/types/object#aborted-summary) * Summary of aborted requests due to aborted execution. The requests in FAILED_N.json _may not_ have been executed. The requests in PENDING_N.json _were not_ executed. ## Calculation #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this calculation. --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique shorthand code for this calculation. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable description of this calculation. --- * ``scope`` - [`CalculationScope!`](/docs/reference/graphql/types/enum#calculation-scope) * The calculation scope of this calculation. --- * ``dimensions`` - [`[PartitionKey]!`]($gql:object:PartitionKey) * Group by these values to index the calculation. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Boolean expression indicating if a balance entry should be written. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the balance calculation was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this calculation. Previous versions are tracked in `history`. --- * ``backfillStatus`` - [`CalculationBackfillStatus!`](/docs/reference/graphql/types/enum#calculation-backfill-status) * The current backfill status. --- * ``status`` - [`CalculationStatus!`](/docs/reference/graphql/types/enum#calculation-status) * The status of this calculation. --- * ``config`` - [`CalculationConfig`](/docs/reference/graphql/types/object#calculation-config) * Config options for this calculation. ## CalculationConfig #### Fields --- * ``enableEffectiveBalances`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Whether this calculation has effective date child calculations. --- * ``effectiveDateSource`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * CEL expression used as the base for effective date dimensions, if set. ## CalculationConnection Connection to a list of Calculation nodes. Access Calculation nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Calculation]!`]($gql:object:Calculation) * --- * ``edges`` - [`[CalculationConnectionEdge]!`]($gql:object:CalculationConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## CalculationConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Calculation`](/docs/reference/graphql/types/object#calculation) * Reference to the Calculation node at this edge. ## CancelBulkQueryExecutionResult Result of a cancelExecution request. #### Fields --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The execution ID that was requested to be cancelled. --- * ``stopping`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Whether a stop request was successfully sent to the execution. ## CancelStatementOutput #### Fields --- * ``status`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. ## Client #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `cl:` where `key` is the `base64(json({ 1: principal }))` --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Principal that this client applies to. If you're supplying your own OIDC this will be the `iss` claim on your JWT. If using Twisp IAM/OIDC token exchange, this will be the IAM principal you signed with, typically a role ARN. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique name of the client. --- * ``policies`` - [`[Policy]!`]($gql:object:Policy) * The policies to evaluate. ## ClientConnection Connection to a list of Client nodes. Access Client nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Client]!`]($gql:object:Client) * --- * ``edges`` - [`[ClientConnectionEdge]!`]($gql:object:ClientConnectionEdge) * Edges represent links connecting a parent or query field to a list of Client nodes. They contain a reference to the Client node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ClientConnectionEdge Edges represent links connecting a parent or query field to a list of Client nodes. They contain a reference to the Client node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Client`](/docs/reference/graphql/types/object#client) * Reference to the Client node at this edge. ## DescribeStatementOutput #### Fields --- * ``createdAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``updatedAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``duration`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``resultRows`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``resultSize`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``status`` - [`SqlStatementStatus!`](/docs/reference/graphql/types/enum#sql-statement-status) * --- * ``hasResultSet`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``database`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dbUser`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``error`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``id`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``queryString`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``workgroupName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``subStatements`` - [`[DescribeStatementOutput_SubStatement]`]($gql:object:DescribeStatementOutput_SubStatement) * ## DescribeStatementOutput_SubStatement #### Fields --- * ``createdAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``updatedAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``duration`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``resultRows`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``resultSize`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``status`` - [`SqlStatementStatus!`](/docs/reference/graphql/types/enum#sql-statement-status) * --- * ``hasResultSet`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``error`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``id`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``queryString`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## DescribeTableOutput #### Fields --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``columnList`` - [`[SQLColumnMetadata]`]($gql:object:SQLColumnMetadata) * ## DocumentElement #### Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Alias for this element. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * The CEL expression for the value of this document element. --- * ``type`` - [`CelType!`](/docs/reference/graphql/types/enum#cel-type) * The type this document element resolves to. --- * ``listOfType`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * If `true` resolves the type as a `[type]` ## Download #### Fields --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of file. e.g. `path/to/file.json` --- * ``downloadURL`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Presigned URL for downloading the file. --- * ``downloadURLExpiration`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of when presigned url expires. --- * ``downloadHeaders`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * Headers to include in the upload request. --- * ``contentType`` - [`String!`](/docs/reference/graphql/types/scalar#string) * `Content-Type` of the file. ## EffectivePeriod A single per-period balance entry returned via `Balance.effectiveBalances` when the parent balance was queried with `effective.range` or `effective.periods`. For `range` queries, each entry represents the activity for that one period. For `periods` queries with `accumulate: false` (default), each entry is the period's activity; with `accumulate: true`, each entry is the cumulative balance through the end of that period. #### Fields --- * ``effective`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The period this entry covers. The format reflects the granularity inferred from the originating query: `YYYY`, `YYYY-MM`, or `YYYY-MM-DD`. --- * ``settled`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts on the settled layer for this period. --- * ``pending`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts on the pending layer for this period. --- * ``encumbrance`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts on the encumbrance layer for this period. --- * ``available`` - [`BalanceAmount!`](/docs/reference/graphql/types/object#balance-amount) * The balance amounts available by combining the provided layer with all layers above. --- * ``dimensions`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * The dimensions of the underlying bucket calculation. --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The calculationId of the underlying bucket. ## Endpoint #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `ep:` where `key` is `base64(json({ 1: endpointId }))`. --- * ``endpointId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the endpoint. --- * ``status`` - [`EndpointStatus!`](/docs/reference/graphql/types/enum#endpoint-status) * Current status for the endpoint. --- * ``endpointType`` - [`EndpointType!`](/docs/reference/graphql/types/enum#endpoint-type) * The type of endpoint this represents. --- * ``url`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The url for the endpoint. e.g. https://yourdomain.com/path/to/hooks --- * ``subscription`` - [`[String!]!`]($gql:scalar:String) * The events this is subscribed to. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of this endpoint. --- * ``signingSecret`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The secret Twisp will use to sign payloads with via HMAC/SHA256. --- * ``filters`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying conditions for sending an event to the endpoint. Record is only sent if _all_ expressions evaluate to true, i.e. they are combined with a logical AND. Each expression must return a boolean value. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * When this endpoint was created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * When this endpoint was last updated. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version of the endpoint. ## EndpointConnection #### Fields --- * ``nodes`` - [`[Endpoint]!`]($gql:object:Endpoint) * --- * ``edges`` - [`[EndpointConnectionEdge]!`]($gql:object:EndpointConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## EndpointConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`Endpoint`](/docs/reference/graphql/types/object#endpoint) * ## Entry An entry represents one side of a transaction in a ledger. In other systems, these may be called "ledger lines" or "journal entries". Entries always have an account, amount, and direction (CREDIT or DEBIT). In addition, Twisp uses the concept of "entry types" to assign every entry to a categorical type. Twisp enforces double-entry accounting, which in practice means that entries can only be entered in the context of a Transaction. Posting a transaction will create _at least 2_ ledger entries. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `en:` where `key` is `base64(json({ 1: entryId }))`. --- * ``entryId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the ledger entry. --- * ``transactionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the transaction which posted this entry. Every entry is associated with a transaction. --- * ``voidOf`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Identifier of the entry this entry voids, when this is a void entry. --- * ``voidedBy`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * Identifier of the entry that voided this entry, when this entry has been voided. --- * ``accountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the account to be debited/credited. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The journal identifier of the ledger entry. --- * ``entryType`` - [`EntryType!`](/docs/reference/graphql/types/scalar#entry-type) * Type code for the entry. --- * ``layer`` - [`Layer!`](/docs/reference/graphql/types/enum#layer) * The layer on which this entry is recorded (SETTLED, PENDING, or ENCUMBRANCE). --- * ``units`` - [`Decimal!`](/docs/reference/graphql/types/scalar#decimal) * Syntactic sugar for `amount { units }`. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * Syntactic sugar for `amount { currency }`. --- * ``amount`` - [`Money!`](/docs/reference/graphql/types/object#money) * Amount of the ledger entry using the currency-supported Money type. --- * ``direction`` - [`DebitOrCredit!`](/docs/reference/graphql/types/enum#debit-or-credit) * The side of the ledger (DEBIT or CREDIT) this entry is posted on. --- * ``sequence`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The order in which this entry was posted within the context of a transaction. This order is auto-generated at time of posting and is determined by the position of the entries posted within the transaction. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of the ledger entry. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Arbitrary structured data about this entry. --- * ``parentAccountIds`` - [`[UUID]`]($gql:scalar:UUID) * Account sets this entry was posted to. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the entry was posted. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this entry. Previous versions are tracked in `history`. --- * ``account`` - [`Account!`](/docs/reference/graphql/types/object#account) * Reference to the account to be debited/credited. --- * ``balance`` - [`Balance`](/docs/reference/graphql/types/object#balance) * Reference to the resulting balance from the entry. --- * ``journal`` - [`Journal!`](/docs/reference/graphql/types/object#journal) * Reference to the journal of the entry. --- * ``history`` - [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) * History of changes to this Entry record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. --- * ``transaction`` - [`Transaction!`](/docs/reference/graphql/types/object#transaction) * Reference to the transaction which posted this entry. ## EntryConnection Connection to a list of Entry nodes. Access Entry nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Entry]!`]($gql:object:Entry) * An entry represents one side of a transaction in a ledger. In other systems, these may be called "ledger lines" or "journal entries". Entries always have an account, amount, and direction (CREDIT or DEBIT). In addition, Twisp uses the concept of "entry types" to assign every entry to a categorical type. Twisp enforces double-entry accounting, which in practice means that entries can only be entered in the context of a Transaction. Posting a transaction will create _at least 2_ ledger entries. --- * ``edges`` - [`[EntryConnectionEdge]!`]($gql:object:EntryConnectionEdge) * Edges represent links connecting a parent or query field to a list of Entry nodes. They contain a reference to the Entry node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## EntryConnectionEdge Edges represent links connecting a parent or query field to a list of Entry nodes. They contain a reference to the Entry node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Entry`](/docs/reference/graphql/types/object#entry) * Reference to the Entry node at this edge. ## ExecuteStatementOutput #### Fields --- * ``createdAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``database`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``dbUser`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``id`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ExecuteStatementSyncOutput #### Fields --- * ``records`` - [`[SQLRecord]`]($gql:object:SQLRecord) * --- * ``columnMetadata`` - [`[SQLColumnMetadata]`]($gql:object:SQLColumnMetadata) * --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``totalNumRows`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## ExecutedSummary #### Fields --- * ``count`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Number of executed requests. --- * ``status`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Of the requests executed, counts of results by status error code. Successful are "OK". If only keys are "OK" and "UNIQUE_KEY_VIOLATION" all the requests in the bulk execution succeeded or already exists. ## Export #### Fields --- * ``id`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``status`` - [`SqlStatementStatus!`](/docs/reference/graphql/types/enum#sql-statement-status) * --- * ``error`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## FailedBulkExecutionError #### Fields --- * ``error`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``cause`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## FileRecord #### Fields --- * ``record`` - [`AchRecord!`](/docs/reference/graphql/types/union#ach-record) * --- * ``recordId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``recordType`` - [`FileRecordType!`](/docs/reference/graphql/types/enum#file-record-type) * --- * ``recordCount`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``recordPosition`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``batchCount`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``batchPosition`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryCount`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``entryPosition`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``execution`` - [`WorkflowExecution`](/docs/reference/graphql/types/object#workflow-execution) * Returns workflow execution backing this file record. ## FileRecordConnection #### Fields --- * ``nodes`` - [`[FileRecord]!`]($gql:object:FileRecord) * --- * ``edges`` - [`[FileRecordConnectionEdge]!`]($gql:object:FileRecordConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## FileRecordConnectionEdge #### Fields --- * ``node`` - [`FileRecord`](/docs/reference/graphql/types/object#file-record) * --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## GetStatementResultOutput #### Fields --- * ``records`` - [`[SQLRecord]`]($gql:object:SQLRecord) * --- * ``columnMetadata`` - [`[SQLColumnMetadata]`]($gql:object:SQLColumnMetadata) * --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``totalNumRows`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## Group Grouping of users within an organization. Groups are used to manage access control and permissions for users. Each group can have one or more associated policies that define the allowed actions for its member users. Users can belong to multiple groups, and their permissions are determined by the combined set of policies from all their groups. #### Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the group. --- * ``organizationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The unique identifier of the organization to which the group belongs. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A human-friendly name for the group, such as 'Admins' or 'DataAnalysts'. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A brief description of the group's purpose, intended to provide additional context. --- * ``policy`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A set of policies as a JSON list that define the permissions granted to users within this group. The structure of these policies matches the Policy type, but serialized as a JSON string. Example: ``` policy: "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"],\"assertions\": {\"always false\": \"1 == 0\"}}]" ``` --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this group. Updates will increment the version. ## GroupConnection Connection to a list of Group nodes. Access Group nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Group]!`]($gql:object:Group) * Grouping of users within an organization. Groups are used to manage access control and permissions for users. Each group can have one or more associated policies that define the allowed actions for its member users. Users can belong to multiple groups, and their permissions are determined by the combined set of policies from all their groups. --- * ``edges`` - [`[GroupConnectionEdge]!`]($gql:object:GroupConnectionEdge) * Edges represent links connecting a parent or query field to a list of Group nodes. They contain a reference to the Group node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## GroupConnectionEdge Edges represent links connecting a parent or query field to a list of Group nodes. They contain a reference to the Group node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Group`](/docs/reference/graphql/types/object#group) * Reference to the Group node at this edge. ## ISO8583Config #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `iso8583:config:`. --- * ``configId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this configuration. --- * ``settlementAccountId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ISO8583 Settlement Account. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Journal to post settlements into. --- * ``timeZone`` - [`String!`](/docs/reference/graphql/types/scalar#string) * IANA Timezone identifier for the configuration. _Example:_ ``"America/Chicago"`` --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Description of this configuration. --- * ``processor`` - [`ISO8583ProcessorType!`](/docs/reference/graphql/types/enum#iso8583processor-type) * Processor type for this configuration. --- * ``processorSpec`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * A moov iso8583 specification. If not passed will use either the default I2C specification (for I2C users) or the ascii87 spec. --- * ``openToBuyConfig`` - [`OpenToBuyConfig!`](/docs/reference/graphql/types/object#open-to-buy-config) * Which balance to use for balance inquiry or partial authorizations. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of when this configuration was created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of the last modification to this configuration. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``history`` - [`ISO8583ConfigHistoryConnection!`](/docs/reference/graphql/types/object#iso8583config-history-connection) * History of changes to this ISO8583 configuration. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. ## ISO8583ConfigConnection Connection to a list of ISO8583 config nodes. Access ISO8583 config nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[ISO8583Config]!`]($gql:object:ISO8583Config) * --- * ``edges`` - [`[ISO8583ConfigConnectionEdge]!`]($gql:object:ISO8583ConfigConnectionEdge) * Edges represent links connecting a parent or query field to a list of ISO8583 config nodes. They contain a reference to the ISO8583 config node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ISO8583ConfigConnectionEdge Edges represent links connecting a parent or query field to a list of ISO8583 config nodes. They contain a reference to the ISO8583 config node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`ISO8583Config`](/docs/reference/graphql/types/object#iso8583config) * Reference to the ISO8583 config node at this edge. ## ISO8583ConfigHistoryConnection Connection to a list of ISO8583 config history nodes. Access ISO8583 config history nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[ISO8583Config]!`]($gql:object:ISO8583Config) * --- * ``edges`` - [`[ISO8583ConfigHistoryConnectionEdge]!`]($gql:object:ISO8583ConfigHistoryConnectionEdge) * Edges represent links connecting a parent or query field to a list of ISO8583 config history nodes. They contain a reference to the ISO8583 config history node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ISO8583ConfigHistoryConnectionEdge Edges represent links connecting a parent or query field to a list of ISO8583 config history nodes. They contain a reference to the ISO8583 config history node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`ISO8583Config`](/docs/reference/graphql/types/object#iso8583config) * Reference to the ISO8583 config history node at this edge. ## Index #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `ix:` where `key` is `base64(json({ 1: name, 2: on }))`. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``on`` - [`IndexOnEnum!`](/docs/reference/graphql/types/enum#index-on-enum) * The type of record this index applies to. --- * ``viewName`` - [`String`](/docs/reference/graphql/types/scalar#string) * Name of the view this index is attached to. Populated only when `on: View`. --- * ``async`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is populated asynchronously. --- * ``unique`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is unique. --- * ``search`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is a search index. --- * ``historical`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is historical, i.e. created with `schema.createHistoricalIndex`. --- * ``partition`` - [`[PartitionKey]`]($gql:object:PartitionKey) * For non-search indexes, the partition key used for this index. --- * ``range`` - [`[IndexKey]`]($gql:object:IndexKey) * For non-search indexes, the range key to use for query/sorting. --- * ``opensearchSchema`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * The Opensearch CEL expression schema to apply to the document prior to indexing. Only available on search indexes. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCateogory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. --- * ``partitionShardCount`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Specifies the number of shards for partition write scaling. This parameter defines how many shards the partition key is automatically split into, similarly to RAID-style disk striping. Increasing this value allows the index to distribute write throughput across multiple shards while sacrificing global sort order on the partition. For instance, setting `partitionShardCount` to 4 splits each unique partition into four shards, effectively allowing 4000 writes per second for a single partition key. --- * ``indexId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Twisp generated internal index identifier. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this index. --- * ``status`` - [`IndexStatus!`](/docs/reference/graphql/types/object#index-status) * ## IndexConnection Connection to a list of Index nodes. Access Index nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Index]!`]($gql:object:Index) * --- * ``edges`` - [`[IndexConnectionEdge]!`]($gql:object:IndexConnectionEdge) * Edges represent links connecting a parent or query field to a list of Index nodes. They contain a reference to the Index node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## IndexConnectionEdge Edges represent links connecting a parent or query field to a list of Index nodes. They contain a reference to the Index node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Index`](/docs/reference/graphql/types/object#index) * Reference to the Index node at this edge. ## IndexKey A named expression used for sorting and range conditions. #### Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Identifier for this key. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression which resolves to the value that is to be sorted. Within the expression, the `document` object represents the record. --- * ``sort`` - [`SortOrder!`](/docs/reference/graphql/types/enum#sort-order) * Whether the sort is in ascending or descending order. --- * ``type`` - [`IndexDataType`](/docs/reference/graphql/types/enum#index-data-type) * Explicit type for sort key. ## IndexStatus #### Fields --- * ``status`` - [`IndexStatusEnum!`](/docs/reference/graphql/types/enum#index-status-enum) * The current state of the index. --- * ``percentageComplete`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The percentage completion of index creation/backfill. ## Journal Journals allow for the organizing of transactions within separate "books". In many cases, users only need a single journal. For this reason, Twisp always contains a default journal with code `DEFAULT`. Journals can be used for a variety of functions. For example, users may create separate journals for different currencies, or product-specific journals. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `jl:` where `key` is `base64(json({ 1: journalId }))`. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the journal. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name for the journal. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of the journal. --- * ``status`` - [`Status!`](/docs/reference/graphql/types/enum#status) * Operational status of the journal. `ACTIVE` journals can be written to with `postTransaction`, whereas `LOCKED` journals do not allow transactions to be posted to them. --- * ``code`` - [`String`](/docs/reference/graphql/types/scalar#string) * Optional unique code for the journal. The default journal uses the code `DEFAULT`. --- * ``config`` - [`JournalConfig!`](/docs/reference/graphql/types/object#journal-config) * Journal specific configuration options for transactions and balances recorded in this journal. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the journal was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this journal. Previous versions are tracked in `history`. --- * ``history`` - [`JournalConnection!`](/docs/reference/graphql/types/object#journal-connection) * History of changes to this Journal record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. ## JournalConfig System configuration for a journal. #### Fields --- * ``enableEffectiveBalances`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * When `true`, records point-in-time effective balances for all accounts in the journal. Defaults to `false`. ## JournalConnection Connection to a list of Journal nodes. Access Journal nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Journal]!`]($gql:object:Journal) * Journals allow for the organizing of transactions within separate "books". In many cases, users only need a single journal. For this reason, Twisp always contains a default journal with code `DEFAULT`. Journals can be used for a variety of functions. For example, users may create separate journals for different currencies, or product-specific journals. --- * ``edges`` - [`[JournalConnectionEdge]!`]($gql:object:JournalConnectionEdge) * Edges represent links connecting a parent or query field to a list of Journal nodes. They contain a reference to the Journal node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## JournalConnectionEdge Edges represent links connecting a parent or query field to a list of Journal nodes. They contain a reference to the Journal node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Journal`](/docs/reference/graphql/types/object#journal) * Reference to the Journal node at this edge. ## KVConnection #### Fields --- * ``nodes`` - [`[KVValue]!`]($gql:object:KVValue) * --- * ``edges`` - [`[KVConnectionEdge]!`]($gql:object:KVConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## KVConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`KVValue`](/docs/reference/graphql/types/object#kvvalue) * ## KVValue #### Fields --- * ``namespace`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Logical grouping key for the record. --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique key within the namespace. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Optional human-readable description stored with the record. --- * ``value`` - [`Value!`](/docs/reference/graphql/types/scalar#value) * Arbitrary JSON payload stored for the record. --- * ``conditions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * CEL conditions evaluated before this version of the record was written, keyed by condition name. Null / empty when no conditions were used. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Record creation timestamp. Preserved across updates. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Record last-modified timestamp. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Current record version. --- * ``history`` - [`KVConnection!`](/docs/reference/graphql/types/object#kvconnection) * Historical versions of this record, returned newest-first. ## Limit #### Fields --- * ``timestampSource`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A literal CEL expression to be evaluated. --- * ``balance`` - [`[BalanceLimit!]!`]($gql:object:BalanceLimit) * ## ListDatabasesOutput #### Fields --- * ``databases`` - [`[String]`]($gql:scalar:String) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ListSchemasOutput #### Fields --- * ``schemas`` - [`[String]`]($gql:scalar:String) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ListStatementsOutput #### Fields --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``statements`` - [`[ListStatementsOutput_Statement]`]($gql:object:ListStatementsOutput_Statement) * ## ListStatementsOutput_Statement #### Fields --- * ``id`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``createdAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. --- * ``isBatchStatement`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``queryString`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``queryStrings`` - [`[String]`]($gql:scalar:String) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``statementName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``status`` - [`SqlStatementStatus!`](/docs/reference/graphql/types/enum#sql-statement-status) * --- * ``updatedAt`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. ## ListTablesOutput #### Fields --- * ``nextToken`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``tables`` - [`[ListTablesOutput_Table]`]($gql:object:ListTablesOutput_Table) * ## ListTablesOutput_Table #### Fields --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``schema`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``type`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## LithicTransactionBalance #### Fields --- * ``transaction`` - [`Transaction!`](/docs/reference/graphql/types/object#transaction) * The transaction that Twisp posted. --- * ``balance`` - [`Balance!`](/docs/reference/graphql/types/object#balance) * The balance for the account. ## Money Money type with multi-currency support. Monetary amounts are represented as decimal units of currency. Fields which use the Money type can be converted to a symbolic representations by specifying a MoneyFormatInput on the `formatted` field. Here is an example table showing different currencies which each have their own divisions of units represented. Japanese yen (JPY) don't have a decimal minor unit, and Bahraini dinars (BHD) use 3 minor unit decimal places. The `formatted` column uses the default values for a an `en-US` locale. | Currency | Units | Formatted | |----------|----------|-----------| | USD | `289.27` | $289.27 | | BHD | `28.927` | 28.927 BD | | JPY | `28927` | ¥28927 | #### Fields --- * ``units`` - [`Decimal!`](/docs/reference/graphql/types/scalar#decimal) * Decimal is a fixed-precision data type supporting exact representation of numeric values. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * ISO 4217 standard three-character code indicating the currency. --- * ``formatted`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## OpenToBuyConfig #### Fields --- * ``openToBuy`` - [`OpenToBuyType!`](/docs/reference/graphql/types/enum#open-to-buy-type) * ## Organization The organization associated with the auth context. Organizations have many tenants, groups, and users. #### Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the organization. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name of the organization. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of the organization. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this organization. ## PageInfo #### Fields --- * ``hasPreviousPage`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * True if there are nodes in the connection before the current page / start cursor. --- * ``hasNextPage`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * True if there are nodes in the connection after the current page / end cursor. --- * ``startCursor`` - [`String`](/docs/reference/graphql/types/scalar#string) * Query cursor for the first node in the current page. --- * ``endCursor`` - [`String`](/docs/reference/graphql/types/scalar#string) * Query cursor for the last node in the current page. ## ParamDefinition Definition of a parameter that can be used when posting transactions using this tran code. These definitions are used to validate the provided `params` in a TransactionInput to ensure that only the right data is applied to the entries created. With CEL, you can access the post-time values of these parameters inside of values in `transaction` and `entries`. #### Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name for the parameter. This is how values passed are accessed. For example, a parameter with name `fromAccount` can be accessed in the `accountId` field of an TranCodeEntryInput with `params.fromAccount`. --- * ``type`` - [`ParamDataType!`](/docs/reference/graphql/types/enum#param-data-type) * Data type for the parameter. --- * ``default`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Default value for the parameter. If not provided, the parameter is consider a 'required' parameter, and a value must be provided when posting a transaction. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Describe the purpose of this parameter. Help an engineer out. --- * ``example`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Example value used for type-checking CEL expressions at tran code creation time. Does NOT provide a runtime default — use `default` for that. If set, the type-checker uses this value instead of `default` to validate expressions. ## PartitionKey A named expression defining a partition key. #### Fields --- * ``alias`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Identifier for this partition key. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression which resolves to the value that is to be used for the partition key. Within the expression, the `document` object represents the record. --- * ``type`` - [`IndexDataType`](/docs/reference/graphql/types/enum#index-data-type) * Resolved type of this partition element. ## Policy #### Fields --- * ``effect`` - [`PolicyEffect!`](/docs/reference/graphql/types/enum#policy-effect) * Whether this Policy is an `ALLOW` or `DENY`. --- * ``actions`` - [`[PolicyAction]!`]($gql:enum:PolicyAction) * The set of actions to allow or deny." --- * ``resources`` - [`[String]!`]($gql:scalar:String) * The resources to allow or deny. --- * ``assertions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * A map of expressions to evaluate this policy with. ## PolicyAssertion #### Fields --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``value`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * A literal CEL expression to be evaluated. ## ResolvedBalanceLimit #### Fields --- * ``layer`` - [`Layer!`](/docs/reference/graphql/types/enum#layer) * Layer at which this balance is balance is enforced. --- * ``amount`` - [`Decimal!`](/docs/reference/graphql/types/scalar#decimal) * Decimal amount of the limit. --- * ``normalBalanceType`` - [`DebitOrCredit!`](/docs/reference/graphql/types/enum#debit-or-credit) * Normal balance direction of this limit. --- * ``start`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of when this limit starts application. --- * ``end`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of when this limit ends application. ## ResolvedLimit #### Fields --- * ``balance`` - [`[ResolvedBalanceLimit!]!`]($gql:object:ResolvedBalanceLimit) * A resolved balance limit. ## ResolvedVelocityControl #### Fields --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier of this control. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Journal this velocity control is acting on. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name of this control --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable description of this control. --- * ``enforcement`` - [`VelocityEnforcement!`](/docs/reference/graphql/types/object#velocity-enforcement) * The enforcement this control produces. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if control should enforce control. The `account`, `transaction`, `balance` and `entry` are available for use in the dimension computation on `context.vars`. @example("has(context.vars.account.metadata.policyPayment)") --- * ``limits`` - [`[ResolvedVelocityLimit!]!`]($gql:object:ResolvedVelocityLimit) * Set of resolved limits for this control. ## ResolvedVelocityControlConnection #### Fields --- * ``nodes`` - [`[ResolvedVelocityControl]!`]($gql:object:ResolvedVelocityControl) * --- * ``edges`` - [`[ResolvedVelocityControlConnectionEdge]!`]($gql:object:ResolvedVelocityControlConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ResolvedVelocityControlConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`ResolvedVelocityControl`](/docs/reference/graphql/types/object#resolved-velocity-control) * ## ResolvedVelocityLimit #### Fields --- * ``calculationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Calculation identifier for checking balances on this resolved velocity limit --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this velocity limit. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name of this limit. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * " Human readable description of this limit. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if an balance entry should be written. The `account`, `transaction` and `entry` are available for use in the dimension computation on `context.vars`. @example("has(context.vars.account.metadata.policyPayment)") --- * ``window`` - [`[PartitionKey!]!`]($gql:object:PartitionKey) * Group by these values to index the calculation. The `account`, `transaction`, `tranCode` and `entry` are available for use in the dimension computation on `context.vars`. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * The currency this limit applies to. If an empty string, applies to all limits. --- * ``params`` - [`[ParamDefinition]`]($gql:object:ParamDefinition) * Parameters for `VelocityLimit.limit` --- * ``limit`` - [`ResolvedLimit!`](/docs/reference/graphql/types/object#resolved-limit) * The resolved values for this velocity limit. ## RestoreOutput #### Fields --- * ``execution_id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. ## RestoreStatus #### Fields --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``status`` - [`RestoreStatusEnum!`](/docs/reference/graphql/types/enum#restore-status-enum) * --- * ``error`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## ResultKeyConnection #### Fields --- * ``nodes`` - [`[String]!`]($gql:scalar:String) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``edges`` - [`[ResultKeyConnectionEdge]!`]($gql:object:ResultKeyConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ResultKeyConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## SQLColumnMetadata #### Fields --- * ``columnDefault`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``isCaseSensitive`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``isCurrency`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``isSigned`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``label`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``length`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``name`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``nullable`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``precision`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``scale`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``schemaName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``tableName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``typeName`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## SQLField #### Fields --- * ``type`` - [`SQLField_Type!`](/docs/reference/graphql/types/enum#sqlfield-type) * --- * ``value`` - [`SQLField_Value`](/docs/reference/graphql/types/object#sqlfield-value) * ## SQLField_Value #### Fields --- * ``isNull`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``bytes`` - [`Uint8Array`](/docs/reference/graphql/types/scalar#uint8array) * Uint8Array is a []uint8 of big-endian encoded binary data --- * ``bool`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. --- * ``double`` - [`Float`](/docs/reference/graphql/types/scalar#float) * The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). --- * ``int`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- * ``str`` - [`String`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## SQLRecord #### Fields --- * ``fields`` - [`[SQLField]`]($gql:object:SQLField) * ## Schedule #### Fields --- * ``jobType`` - [`JobType!`](/docs/reference/graphql/types/enum#job-type) * The job type to create the schedule for. Currently only one schedule per job-type is supported. --- * ``jobName`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A job name that's unique per job type. --- * ``principal`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The Twisp principal to run the job on a schedule. This should have a matching `client` policy. --- * ``scheduleExpression`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A schedule expression to run this job on. cron/rate/once supported see https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html for valid syntax. --- * ``timezone`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The timezone to run this schedule on based on https://www.iana.org/time-zones example: "America/Los_Angeles" or "UTC" Supports ST rules defined in https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html --- * ``metadata`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * JSON metadata to pass to running job. ## Tenant A Tenant represents an environment within an organization, typically associated with a specific application, service, or set of resources. Tenants contain isolated ledgers, each deployed to a specific region. Tenants are useful for isolating data and configurations between different environments. Each tenant is uniquely identified by an accountId, which in combination with an AWS region, is used to calculate the database tenant for data isolation purposes. #### Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the tenant. --- * ``organizationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the tenant's parent organization. --- * ``accountId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A globally unique identifier representing an environment within the organization. This accountId, when combined with an AWS region, is used to calculate the database tenant. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A human-friendly name for the tenant, used for display purposes and easier identification. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * A brief description of the tenant, providing additional context about its purpose or characteristics. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this tenant. Updates will increment the version. ## TenantConnection Connection to a list of Tenant nodes. Access Tenant nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Tenant]!`]($gql:object:Tenant) * A Tenant represents an environment within an organization, typically associated with a specific application, service, or set of resources. Tenants contain isolated ledgers, each deployed to a specific region. Tenants are useful for isolating data and configurations between different environments. Each tenant is uniquely identified by an accountId, which in combination with an AWS region, is used to calculate the database tenant for data isolation purposes. --- * ``edges`` - [`[TenantConnectionEdge]!`]($gql:object:TenantConnectionEdge) * Edges represent links connecting a parent or query field to a list of Tenant nodes. They contain a reference to the Tenant node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## TenantConnectionEdge Edges represent links connecting a parent or query field to a list of Tenant nodes. They contain a reference to the Tenant node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Tenant`](/docs/reference/graphql/types/object#tenant) * Reference to the Tenant node at this edge. ## TranCode Transaction Codes (tran codes) are how financial engineers do double-entry accounting. They encode the basic patterns for a type of transaction as a predictable and repeatable formula. You can think of tran codes as function signatures which define how a transaction acts upon the ledger. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `tc:` where `key` is `base64(json({ 1: tranCodeId }))`. --- * ``tranCodeId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Internal UUID for the transaction code record. --- * ``code`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The tran code represented as a unique string identifier. The code itself is a shorthand for the behavior represented. For example, the code `ACH_CREDIT` may represent a transaction writing two entries: an `ACH_DR` entry and an `ACH_CR` entry. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Explanation of what this tran code represents and how it should be used. This provides documentation for the tran code. --- * ``params`` - [`[ParamDefinition]`]($gql:object:ParamDefinition) * Defines the parameters that can be used when posting transactions using this tran code. --- * ``transaction`` - [`TranCodeTransaction!`](/docs/reference/graphql/types/object#tran-code-transaction) * Definition of the transaction posted when this tran code is invoked. --- * ``entries`` - [`[TranCodeEntry!]!`]($gql:object:TranCodeEntry) * Definition of the entries written when transactions are posted with this tran code. --- * ``status`` - [`Status!`](/docs/reference/graphql/types/enum#status) * Operational status of the tran code. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Metadata attached to this tran code. --- * ``vars`` - [`ExpressionNestedMap`](/docs/reference/graphql/types/scalar#expression-nested-map) * CEL expressions that are evaluated before transaction and entries and can be used a scratch pad area. --- * ``assertions`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Named boolean CEL expressions that must all evaluate to true when posting a transaction. Evaluated after params and vars are resolved. Failures return BadRequest. --- * ``workflow`` - [`TranCodeWorkflow`](/docs/reference/graphql/types/object#tran-code-workflow) * Workflow execution definition triggered when this tran code is invoked. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the tran code was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this account. Previous versions are tracked in `history`. --- * ``history`` - [`TranCodeConnection!`](/docs/reference/graphql/types/object#tran-code-connection) * History of changes to this TranCode record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. ## TranCodeConnection Connection to a list of TranCode nodes. Access TranCode nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[TranCode]!`]($gql:object:TranCode) * Transaction Codes (tran codes) are how financial engineers do double-entry accounting. They encode the basic patterns for a type of transaction as a predictable and repeatable formula. You can think of tran codes as function signatures which define how a transaction acts upon the ledger. --- * ``edges`` - [`[TranCodeConnectionEdge]!`]($gql:object:TranCodeConnectionEdge) * Edges represent links connecting a parent or query field to a list of TranCode nodes. They contain a reference to the TranCode node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## TranCodeConnectionEdge Edges represent links connecting a parent or query field to a list of TranCode nodes. They contain a reference to the TranCode node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`TranCode`](/docs/reference/graphql/types/object#tran-code) * Reference to the TranCode node at this edge. ## TranCodeEntry Definition of an entry written when transactions are posted with this tran code. #### Fields --- * ``entryType`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Entry type for an entry written when this tran code is invoked. --- * ``accountId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Account ID for an entry written when this tran code is invoked. --- * ``layer`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Layer for an entry written when this tran code is invoked. --- * ``direction`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Direction for an entry written when this tran code is invoked. --- * ``units`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Units of currency for an entry written when this tran code is invoked. --- * ``currency`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Currency used for an entry written when this tran code is invoked. --- * ``description`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Description for an entry written when this tran code is invoked. --- * ``metadata`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Metadata for entries posted with this tran code. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression that indicates if this entry should be written. @example("params.amount > decimal(0.00)") ## TranCodeTransaction Definition of the transaction posted when this tran code is invoked. #### Fields --- * ``effective`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Effective date for transactions posted with this tran code. --- * ``journalId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Journal ID for transactions posted with this tran code. --- * ``correlationId`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Correlation ID for transactions posted with this tran code. --- * ``externalId`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * External ID for transactions posted with this tran code. --- * ``description`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Description for transactions posted with this tran code. --- * ``metadata`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * Metadata for transactions posted with this tran code. ## TranCodeWorkflow Definition for workflow execution triggered by a tran code. #### Fields --- * ``workflowId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression that evaluates to the workflow ID (UUID). --- * ``executionId`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression that evaluates to the execution ID (UUID). --- * ``task`` - [`Expression!`](/docs/reference/graphql/types/scalar#expression) * CEL expression that evaluates to the task name (String). --- * ``params`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of param names to CEL expressions for workflow params. ## Transaction Transactions record all accounting events in the ledger. In Twisp, the only way to write to a ledger is through a transaction. Every transaction writes two or more entries to the ledger in standard double-entry accounting practice. Twisp expands upon the basic principle of an accounting transaction with additional features like transaction codes and correlations. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `tx:` where `key` is `base64(json({ 1: transactionId }))`. --- * ``transactionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the transaction. --- * ``tranCodeId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the tran code used by this transaction. --- * ``journalId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for the journal this transaction applies to. --- * ``correlationId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Allows related transactions to be grouped. When a transaction is posted without a `correlationId`, it uses the `transactionId` as the `correlationId`. Then, future related transactions can be posted with the same `correlationId` to indicate their relationship to the original. This is very useful for events like holds, auths, auth reversals, etc. For example, consider the following (simplified) list of transactions: (ID: 1) Place card hold for $50 on account A (correlation ID: 1) (ID: 2) Place card hold for $20 on account B (correlation ID: 2) (ID: 3) Release card hold for $50 on account A (correlation ID: 1) Because transaction (3) is _related_ to transaction (1), it shares the same correlation ID. This way, we can easily observe the entire history of a multi-transaction event by querying the correlated transactions. --- * ``externalId`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Allows specifying a unique external ID associated with this transaction. --- * ``effective`` - [`Date!`](/docs/reference/graphql/types/scalar#date) * The effective date records when the transaction is recorded as occurring for accounting purposes. Determines the accounting period within which the transaction is counted. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Description of the transaction. --- * ``metadata`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Arbitrary structured data about this transaction. --- * ``voidedBy`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * The voided by records the transaction identifier that voided this transaction. --- * ``voidOf`` - [`UUID`](/docs/reference/graphql/types/scalar#uuid) * The void of records the transaction identifier this transaction is voiding --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the transaction was first posted. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``properties`` - [`TransactionProperties!`](/docs/reference/graphql/types/object#transaction-properties) * --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this transaction. Previous versions are tracked in `history`. --- * ``correlated`` - [`TransactionConnection!`](/docs/reference/graphql/types/object#transaction-connection) * List of all correlated transactions. These are transactions which share the same `correlationId`. --- * ``entries`` - [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) * Ledger entries written by the transaction. --- * ``history`` - [`TransactionConnection!`](/docs/reference/graphql/types/object#transaction-connection) * History of changes to this Transaction record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. --- * ``journal`` - [`Journal!`](/docs/reference/graphql/types/object#journal) * Reference to the journal this transaction applies to. --- * ``tranCode`` - [`TranCode!`](/docs/reference/graphql/types/object#tran-code) * Reference to the tran code used by this transaction. --- * ``exceptions`` - [`[TransactionException]`]($gql:object:TransactionException) * Reference to any exceptions, if occurred. --- * ``workflows`` - [`WorkflowExecutionConnection`](/docs/reference/graphql/types/object#workflow-execution-connection) * Look up the workflow executions that produced this transaction. ## TransactionConnection Connection to a list of Transaction nodes. Access Transaction nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[Transaction]!`]($gql:object:Transaction) * Transactions record all accounting events in the ledger. In Twisp, the only way to write to a ledger is through a transaction. Every transaction writes two or more entries to the ledger in standard double-entry accounting practice. Twisp expands upon the basic principle of an accounting transaction with additional features like transaction codes and correlations. --- * ``edges`` - [`[TransactionConnectionEdge]!`]($gql:object:TransactionConnectionEdge) * Edges represent links connecting a parent or query field to a list of Transaction nodes. They contain a reference to the Transaction node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## TransactionConnectionEdge Edges represent links connecting a parent or query field to a list of Transaction nodes. They contain a reference to the Transaction node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`Transaction`](/docs/reference/graphql/types/object#transaction) * Reference to the Transaction node at this edge. ## TransactionException #### Fields --- * ``type`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``message`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``detail`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * JSON object. ## TransactionProperties #### Fields --- * ``overrideVelocityEnforcement`` - [`VelocityEnforcement`](/docs/reference/graphql/types/object#velocity-enforcement) * Overrides velocity enforcements that have an action of `VOID` or `REJECT`. Useful for forced postings that require disabled velocity control enforcement (e.g. set to `WARN`). ## Upload #### Fields --- * ``key`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of file. e.g. `path/to/file.json` --- * ``uploadURL`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Presigned URL for uploading the actual file. --- * ``uploadURLExpiration`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Timestamp of when presigned url expires. If expired before file uploaded will need to create new upload. --- * ``uploadHeaders`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * Headers to include in the upload request. --- * ``contentType`` - [`String!`](/docs/reference/graphql/types/scalar#string) * `contentType` of file. Currently only type supported is `application/json`. File should be array of json objects to pass to bulk query as variables, each item in array representing a single execution of the query. --- * ``contentEncoding`` - [`String`](/docs/reference/graphql/types/scalar#string) * `Content-Encoding` of the file. When set, the upload should use this encoding (e.g. `gzip`). ## Usage #### Fields --- * ``readUnits`` - [`[UsageDatum]`]($gql:object:UsageDatum) * The number of read units in the period listed. --- * ``writeUnits`` - [`[UsageDatum]`]($gql:object:UsageDatum) * The number of write units in the period listed. --- * ``warehouseRPUSeconds`` - [`[UsageDatum]`]($gql:object:UsageDatum) * The number of RPU seconds charged. Twisp bills per hour so divide `warehouseRPUSeconds.units / 3600` for billing purposes. ## UsageDatum #### Fields --- * ``period`` - [`Date`](/docs/reference/graphql/types/scalar#date) * Date in YYYY-MM-DD format. --- * ``units`` - [`Int`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## User A human user within the organization. Users can belong to multiple groups, which define their permissions within the organization based on the associated policies of each group. The user's effective permissions are determined by the combined set of policies from all their groups. A user is uniquely identified by their email address. #### Fields --- * ``id`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique ID for the user. --- * ``organizationId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * ID of the user's parent organization. --- * ``groupIds`` - [`[UUID!]!`]($gql:scalar:UUID) * A list of unique identifiers for the groups to which the user belongs. The user's permissions are determined by the combined policies of these groups. --- * ``email`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The user's email address, which serves as a unique identifier and primary means of contact. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this user. Updates will increment the version. --- * ``mfaEnabled`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * Returns if the users MFA is enabled. ## UserConnection Connection to a list of User nodes. Access User nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[User]!`]($gql:object:User) * A human user within the organization. Users can belong to multiple groups, which define their permissions within the organization based on the associated policies of each group. The user's effective permissions are determined by the combined set of policies from all their groups. A user is uniquely identified by their email address. --- * ``edges`` - [`[UserConnectionEdge]!`]($gql:object:UserConnectionEdge) * Edges represent links connecting a parent or query field to a list of User nodes. They contain a reference to the User node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## UserConnectionEdge Edges represent links connecting a parent or query field to a list of User nodes. They contain a reference to the User node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`User`](/docs/reference/graphql/types/object#user) * Reference to the User node at this edge. ## VelocityBalance #### Fields --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The matching velocity control id --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * The matching velocity limit. --- * ``spent`` - [`Decimal!`](/docs/reference/graphql/types/scalar#decimal) * The amount spent on the limit. --- * ``remaining`` - [`Decimal!`](/docs/reference/graphql/types/scalar#decimal) * The amount remaining on the limit. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * The currency of this velocity balance. --- * ``velocityLimit`` - [`VelocityLimit!`](/docs/reference/graphql/types/object#velocity-limit) * Get the velocity limit for this Balance. --- * ``balance`` - [`Balance!`](/docs/reference/graphql/types/object#balance) * The underlying balance for this velocity. --- * ``entries`` - [`EntryConnection!`](/docs/reference/graphql/types/object#entry-connection) * Retrieve the entries in this window. ## VelocityControl #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. --- * ``velocityControlId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier of this control. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name of this control. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable description of this control. --- * ``enforcement`` - [`VelocityEnforcement!`](/docs/reference/graphql/types/object#velocity-enforcement) * The the enforcement this control produces. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if control should enforce. The `account`, `transaction`, `balance` and `entry` are available for use in the dimension computation on `context.vars`. @example("has(context.vars.account.metadata.policyPayment)") --- * ``limits`` - [`[VelocityLimit]!`]($gql:object:VelocityLimit) * Set of default velocity limits for this control. ## VelocityControlConnection #### Fields --- * ``nodes`` - [`[VelocityControl]!`]($gql:object:VelocityControl) * --- * ``edges`` - [`[VelocityControlConnectionEdge]!`]($gql:object:VelocityControlConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## VelocityControlConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`VelocityControl`](/docs/reference/graphql/types/object#velocity-control) * ## VelocityEnforcement #### Fields --- * ``action`` - [`VelocityEnforcementAction!`](/docs/reference/graphql/types/enum#velocity-enforcement-action) * ## VelocityLimit #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. --- * ``velocityLimitId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Unique identifier for this velocity limit. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Human readable name of this limit. --- * ``description`` - [`String!`](/docs/reference/graphql/types/scalar#string) * " Human readable description of this limit. --- * ``window`` - [`[PartitionKey]!`]($gql:object:PartitionKey) * Group by these values to index the calculation. The `account`, `transaction`, `tranCode` and `entry` are available for use in the dimension computation on `context.vars`. --- * ``condition`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * A boolean expression indicating if an balance entry should be written. The `account`, `transaction` and `entry` are available for use in the dimension computation on `context.vars`. @example("has(context.vars.account.metadata.policyPayment)") --- * ``limit`` - [`Limit!`](/docs/reference/graphql/types/object#limit) * The limit to enforce. --- * ``currency`` - [`CurrencyCode!`](/docs/reference/graphql/types/scalar#currency-code) * The currency this limit applies to. If an empty string, applies to all limits. --- * ``params`` - [`[ParamDefinition]`]($gql:object:ParamDefinition) * Parameters for `VelocityLimit.limit` ## VelocityLimitConnection #### Fields --- * ``nodes`` - [`[VelocityLimit]!`]($gql:object:VelocityLimit) * --- * ``edges`` - [`[VelocityLimitConnectionEdge]!`]($gql:object:VelocityLimitConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## VelocityLimitConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`VelocityLimit`](/docs/reference/graphql/types/object#velocity-limit) * ## View Represents an view materialized view. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for a record to support Global Object Identification. Uses format `ag:`. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this view. Typically human readable. --- * ``document`` - [`[DocumentElement!]!`]($gql:object:DocumentElement) * The document produced by this view. --- * ``sources`` - [`[ViewSource!]!`]($gql:object:ViewSource) * List of source tables that trigger updates to this view. --- * ``partition`` - [`[PartitionKey!]!`]($gql:object:PartitionKey) * Dimensions define the partition for the view. --- * ``sort`` - [`[IndexKey!]!`]($gql:object:IndexKey) * The sort key to use for supporting range queries. --- * ``filters`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Filters that determine when the view should be updated. Only source changes that satisfy these conditions will trigger an update. --- * ``description`` - [`String`](/docs/reference/graphql/types/scalar#string) * Human readable description of this view. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Creation timestamp of this view. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Lat modified timestamp of this view. --- * ``config`` - [`ViewConfig!`](/docs/reference/graphql/types/object#view-config) * Configuration including concurrent enablement. --- * ``viewId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Twisp generated internal view identifier. --- * ``normalize`` - [`Expression`](/docs/reference/graphql/types/scalar#expression) * Expression that this view is normalized by. --- * ``indexes`` - [`[ViewIndex]`]($gql:object:ViewIndex) * --- * ``searchIndexes`` - [`[ViewSearchIndex]`]($gql:object:ViewSearchIndex) * --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this view. ## ViewConfig #### Fields --- * ``enableConcurrentPosting`` - [`Boolean!`](/docs/reference/graphql/types/scalar#boolean) * The `Boolean` scalar type represents `true` or `false`. ## ViewConnection Connection to a list of View nodes. Access View nodes directly through the `nodes` field, or access information about the connection edges with the `edges` field. Use `pageInfo` to paginate responses using the cursors provided. #### Fields --- * ``nodes`` - [`[View]!`]($gql:object:View) * Represents an view materialized view. --- * ``edges`` - [`[ViewConnectionEdge]!`]($gql:object:ViewConnectionEdge) * Edges represent links connecting a parent or query field to a list of View nodes. They contain a reference to the View node and metadata like the `cursor` position for the edge. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ViewConnectionEdge Edges represent links connecting a parent or query field to a list of View nodes. They contain a reference to the View node and metadata like the `cursor` position for the edge. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`View`](/docs/reference/graphql/types/object#view) * Reference to the View node at this edge. ## ViewIndex #### Fields --- * ``indexId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Twisp generated internal index identifier. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``unique`` - [`Boolean`](/docs/reference/graphql/types/scalar#boolean) * Indicates if this index is unique. --- * ``partition`` - [`[PartitionKey]!`]($gql:object:PartitionKey) * The partition key used for this index. --- * ``partitionShardCount`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * Specifies the number of shards for partition write scaling. This parameter defines how many shards the partition key is automatically split into, similarly to RAID-style disk striping. Increasing this value allows the index to distribute write throughput across multiple shards while sacrificing global sort order on the partition. For instance, setting `partitionShardCount` to 4 splits each unique partition into four shards, effectively allowing 4000 writes per second for a single partition key. --- * ``sort`` - [`[IndexKey]!`]($gql:object:IndexKey) * The sort key to use for supporting range queries. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCategory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. ## ViewRecord Represents a single entry in an view materialized view. #### Fields --- * ``id`` - [`ID!`](/docs/reference/graphql/types/scalar#id) * Globally unique identifier for this view entry. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Name of the view this record is from. --- * ``document`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * The document content of this view entry. Structure depends on the view definition. --- * ``source`` - [`ViewTrigger!`](/docs/reference/graphql/types/object#view-trigger) * Information about the data source that trigged this version of the document. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The current version number of this view. --- * ``history`` - [`ViewRecordConnection!`](/docs/reference/graphql/types/object#view-record-connection) * History of changes to this View record. Because ledgers are immutable and append-only, all changes are recorded as sequenced versions of the record, providing an unbroken lineage of the current state. ## ViewRecordConnection Connection to a list of ViewEntry nodes. #### Fields --- * ``nodes`` - [`[ViewRecord]!`]($gql:object:ViewRecord) * Represents a single entry in an view materialized view. --- * ``edges`` - [`[ViewRecordConnectionEdge]!`]($gql:object:ViewRecordConnectionEdge) * Edges represent links connecting a parent or query field to a list of ViewEntry nodes. --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## ViewRecordConnectionEdge Edges represent links connecting a parent or query field to a list of ViewEntry nodes. #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Cursor position at this edge. --- * ``node`` - [`ViewRecord`](/docs/reference/graphql/types/object#view-record) * Reference to the ViewEntry node at this edge. ## ViewSearchIndex #### Fields --- * ``indexId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Twisp generated internal index identifier. --- * ``name`` - [`String!`](/docs/reference/graphql/types/scalar#string) * Unique identifier of this index. Typically human readable. --- * ``constraints`` - [`ExpressionMap`](/docs/reference/graphql/types/scalar#expression-map) * Map of named CEL expressions specifying the conditions for including a record in this index. Records are only included in the index if _all_ expressions evaluate to `true`, i.e. they are combined with a logical AND. Each expression must return a boolean value. For example, a custom index on a `metadata.category` field might use the constraints `{ hasCategory: "has(document.metadata.category)" }` to ensure that only records whose `metadata` document has a defined value for the `category` field are included. --- * ``opensearchSchema`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * The Opensearch CEL expression schema to apply to the document prior to indexing. Only available on search indexes. ## ViewSource #### Fields --- * ``entity`` - [`ViewEntity!`](/docs/reference/graphql/types/enum#view-entity) * Enum of source tables that can trigger view updates. --- * ``triggers`` - [`[ViewTriggerEnum!]!`]($gql:enum:ViewTriggerEnum) * ## ViewTrigger #### Fields --- * ``entity`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``trigger`` - [`ViewTriggerEnum!`](/docs/reference/graphql/types/enum#view-trigger-enum) * --- * ``new`` - [`DatabaseEntity!`](/docs/reference/graphql/types/union#database-entity) * Resolve the new trigger entity that created this version of the document. --- * ``old`` - [`DatabaseEntity`](/docs/reference/graphql/types/union#database-entity) * Resolve the old trigger entity that created this version of the document. ## WorkflowActivity #### Fields --- * ``action`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entity`` - [`WorkflowEntity!`](/docs/reference/graphql/types/union#workflow-entity) * --- * ``entityId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``entityType`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## WorkflowExecution #### Fields --- * ``workflowId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Workflow Id of workflow invoked. --- * ``executionId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * Execution Id of this execution. --- * ``task`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The task that was invoked on this version of the workflow execution. --- * ``params`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Parameters supplied for this workflow execution. --- * ``context`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * Context to pass along to future executions of this workflow. --- * ``output`` - [`WorkflowExecutionOutput!`](/docs/reference/graphql/types/object#workflow-execution-output) * Output of this invocation of the workflow. --- * ``error`` - [`String`](/docs/reference/graphql/types/scalar#string) * Errors from this invocation of the workflow. --- * ``created`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Date and time when the execution was first created. --- * ``modified`` - [`Timestamp!`](/docs/reference/graphql/types/scalar#timestamp) * Time of the last change. Especially useful when reviewing the `history`. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * version number of the execution. Previous versions are tracked in history. --- * ``activities`` - [`[WorkflowActivity]`]($gql:object:WorkflowActivity) * Activities that this workflow took. It may have inserted entities like Transactions or invoked Workflows. --- * ``history`` - [`WorkflowExecutionConnection!`](/docs/reference/graphql/types/object#workflow-execution-connection) * Workflow execution history for this workflow. --- * ``state`` - [`JSON!`](/docs/reference/graphql/types/scalar#json) * Outputs the execution state of the workflow. Depending on the workflow will contain diagnostic detials specific to the workflow. --- * ``transactions`` - [`[Transaction]`]($gql:object:Transaction) * Resolve any posted transactions that this workflow posted in this invocation. ## WorkflowExecutionConnection #### Fields --- * ``nodes`` - [`[WorkflowExecution]!`]($gql:object:WorkflowExecution) * --- * ``edges`` - [`[WorkflowExecutionConnectionEdge]!`]($gql:object:WorkflowExecutionConnectionEdge) * --- * ``pageInfo`` - [`PageInfo!`](/docs/reference/graphql/types/object#page-info) * ## WorkflowExecutionConnectionEdge #### Fields --- * ``cursor`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``node`` - [`WorkflowExecution`](/docs/reference/graphql/types/object#workflow-execution) * ## WorkflowExecutionOutput #### Fields --- * ``state`` - [`JSON`](/docs/reference/graphql/types/scalar#json) * JSON object. --- * ``entities`` - [`[WorkflowExecutionOutputEntity]`]($gql:object:WorkflowExecutionOutputEntity) * ## WorkflowExecutionOutputEntity #### Fields --- * ``action`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``entity`` - [`WorkflowEntity!`](/docs/reference/graphql/types/union#workflow-entity) * --- * ``entityId`` - [`UUID!`](/docs/reference/graphql/types/scalar#uuid) * 128-bit universally unique identifier (UUID). Used for most ID fields on records. --- * ``entityType`` - [`String!`](/docs/reference/graphql/types/scalar#string) * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. --- * ``version`` - [`Int!`](/docs/reference/graphql/types/scalar#int) * The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. --- # Scalar Types Scalar types represent primitive values like strings, integers, and booleans. Source: https://www.twisp.com/docs/reference/graphql/types/scalar ## Boolean The `Boolean` scalar type represents `true` or `false`. ## CurrencyCode ISO 4217 standard three-character code indicating the currency. _Examples:_ - ``'USD'`` - ``'CHF'`` ## Date Date in YYYY-MM-DD format. _Example:_ ``'2022-08-18'`` ## Decimal Decimal is a fixed-precision data type supporting exact representation of numeric values. _Example:_ ``105.92851`` ## EntryType String value for an entry type. _Example:_ ``'ACH_CR'`` ## Expression A literal CEL expression to be evaluated. ## ExpressionMap A map of literal CEL expressions to be evaluated in a shared context Ex: { "two": "this.one + 1", "one": "2 - 1", "sqrt2": "math.Sqrt(double(2))", "now": "time.Now()" } ## ExpressionNestedMap A nested map of literal CEL expressions to be evaluated in a shared context Ex: { "two": "this.one + 1", "one": "2 - 1", "sqrt2": "math.Sqrt(double(2))", "now": "time.Now()", "obj": { "foo": "'bar'" } } ## ExpressionValue A Value-shaped CEL template. String leaves are CEL expressions; objects and lists recurse; non-string leaves are literal values. ## Float The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). ## ID The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. ## Int The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ## InterpolatedExpression Interpolated string expression. Values within `{{}}` are evaluated as a CEL expression. Examples: - `Current time: {{time.Now()}}` => `"Current time: 2022-04-27T10:50:00.000Z"` - `Raw String` => `"Raw String"` - `{{uuid.New()}}` => `"9dd984db-78d8-420f-9380-80e3cf36fe75"` ## JSON JSON object. _Example:_ ```{ "counts": 12, "name": "Metric A" }``` ## String The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. ## Timestamp [RFC3339-compliant](https://www.rfc-editor.org/rfc/rfc3339) UTC timestamp. _Example:_ ``'2022-04-27T10:50:00.000Z'`` ## UUID 128-bit universally unique identifier (UUID). Used for most ID fields on records. _Example:_ ``'3ea12e45-7df2-4293-9434-feb792affc91'`` ## Uint8Array Uint8Array is a []uint8 of big-endian encoded binary data ## Value Value object. Similar to the JSON object with support for type coersion _Example:_ ```{ "int": 12, "float": 1.732, "uuid": "D3DC5ED3-23D0-4924-BAE1-9AA026BACE09"}``` --- # Union Types Union types combine two or more object types into a single type to represent data that can be one of multiple types. Source: https://www.twisp.com/docs/reference/graphql/types/union ## AccountSetMember Account set members can be of type Account or AccountSet. #### Possible Types - [`Account`](/docs/reference/graphql/types/object#account) - [`AccountSet`](/docs/reference/graphql/types/object#account-set) ## AchRecord #### Possible Types - [`AchFile`](/docs/reference/graphql/types/object#ach-file) - [`AchBatch`](/docs/reference/graphql/types/object#ach-batch) - [`AchIatBatch`](/docs/reference/graphql/types/object#ach-iat-batch) - [`AchEntryDetail`](/docs/reference/graphql/types/object#ach-entry-detail) - [`AchAdvEntryDetail`](/docs/reference/graphql/types/object#ach-adv-entry-detail) - [`AchIatEntryDetail`](/docs/reference/graphql/types/object#ach-iat-entry-detail) ## DatabaseEntity #### Possible Types - [`Account`](/docs/reference/graphql/types/object#account) - [`AccountSet`](/docs/reference/graphql/types/object#account-set) - [`Balance`](/docs/reference/graphql/types/object#balance) - [`Entry`](/docs/reference/graphql/types/object#entry) - [`Transaction`](/docs/reference/graphql/types/object#transaction) - [`TranCode`](/docs/reference/graphql/types/object#tran-code) ## WorkflowEntity #### Possible Types - [`Transaction`](/docs/reference/graphql/types/object#transaction) - [`WorkflowExecution`](/docs/reference/graphql/types/object#workflow-execution) - [`AchWorkflowTrace`](/docs/reference/graphql/types/object#ach-workflow-trace) --- # Reference Definitions of all components in the Twisp Accounting Core and GraphQL API. Source: https://www.twisp.com/docs/reference ## Reference Sections - [Ledger](/docs/reference/ledger): Ledger resources in the Twisp accounting core. - [ACH](/docs/reference/ach): ACH resources in the Twisp accounting core. - [GraphQL](/docs/reference/graphql): Type definitions for the full GraphQL schema. - [API](/docs/reference/api): Key components and concepts for interacting with the API. - [CEL](/docs/reference/cel): Packages and functions available in the common expression language runtime. ## Type Diagrams Use these diagrams for a high-level overview of the type system within the Twisp ledger. ### Basic Relationships Simplified type relationship diagram showing basic connections between types. - [Journals](/docs/reference/graphql/types/object#journal) have many [Transactions](/docs/reference/graphql/types/object#transaction) and [Entries](/docs/reference/graphql/types/object#entry) - [Transactions](/docs/reference/graphql/types/object#transaction) are defined by their [TranCode](/docs/reference/graphql/types/object#tran-code) and write multiple [Entries](/docs/reference/graphql/types/object#entry) to the ledger. - [Transactions](/docs/reference/graphql/types/object#transaction) can be linked to other correlated [Transactions](/docs/reference/graphql/types/object#transaction) - [Entries](/docs/reference/graphql/types/object#entry) are written to a specific [Account](/docs/reference/graphql/types/object#account) and [Journal](/docs/reference/graphql/types/object#journal) - [Accounts](/docs/reference/graphql/types/object#account) roll up a [Balance](/docs/reference/graphql/types/object#balance) of all [Entries](/docs/reference/graphql/types/object#entry) for each [Journal](/docs/reference/graphql/types/object#journal) - [Account Sets](/docs/reference/graphql/types/object#account-set) are groupings of [Accounts](/docs/reference/graphql/types/object#account) and/or other [Account Sets](/docs/reference/graphql/types/object#account-set) which also roll up [Balances](/docs/reference/graphql/types/object#balance) ![Simplified entity relationship diagram for Twisp core](/docs/images/diagrams/core_erd_minimal.svg) ### Entity Relationship Diagram Although the Twisp core is not a relational database, it does enforce referential integrity between related records and thus we can model it with an RDB-style ERD. Only select fields for each type have been shown in this diagram to aid readability. For the full reference of each type, see the [GraphQL reference](/docs/reference/graphql). ![RDB-style entity relationship diagram for Twisp core](/docs/images/diagrams/core_erd.svg) --- # Account Sets Reference for the account set resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/account-sets ## The Basics Account sets allow you to group and organize accounts within the Twisp Accounting Core, providing a structured way to manage and visualize your chart of accounts. > **Note:** > > Account Sets are how you create **custom groupings of accounts** for better organization and **multi-account balance materializations**. Instead of using a single hierarchical tree to model your chart of accounts, account sets offer a more dynamic approach. They are custom groups of accounts that aggregate balances and provide a unified interface into the entries for all accounts within the set. ## Components of Account Sets There are 5 key components that make up account sets: 1. **Members**: either accounts or other account sets. This flexibility allows you to create more complex structures for your chart of accounts, as you can nest sets within other sets. 2. **Sets**: reference the other sets (if any) which contain this set as a member. 3. **Balances**: represent the sum of all balances of member accounts and member account sets. Balances are computed for every currency and layer used by the entries posted to accounts in a set and all of its sub-sets. 4. **Journal**: associates the account set with a specific journal. Account sets only compute balances using entries posted to their associated journal. 5. **Normal Balance Type**: determines how the account set's normal balances are computed, just like for accounts. In addition, account sets have other properties which are common to most or all resources in the accounting core: - **ID**: a universally unique identifier (UUID) for the account set. - **Name**: a descriptive name to identify the account set. - **Description**: a free-form text to be used for describing anything about the account set. We recommend using this field to summarize what the account set is for and when it should be used. - **Metadata**: unstructured, user-specified JSON. Can be combined with custom indexes for powerful querying capacities. - **Config**: allows setting up an account set for concurrent posting. - **Created & Updated Timestamps**: self-evident: when the account set was created and when it was last updated. - **Version & History**: account sets, like every other record in the accounting core, maintain a list of all changes made to them in their `history` field, and the `version` field indicates the current active version of the account set. ## Nesting and Hierarchies in Account Sets Account sets in Twisp offer a flexible way to organize accounts into hierarchical structures. By nesting account sets within other account sets, complex structures can be modeled to better fit the needs of your business or organization. Nesting account sets is as simple as using the `addToAccountSet` mutation and specifying the `memberType` as `ACCOUNT_SET`. Here's an example: ```graphql mutation AddToAccountSetNested( addToAccountSet( id: "" member: { memberId: "", memberType: ACCOUNT_SET } ) { accountSetId members(first: 10) { nodes { __typename ... on AccountSet { accountSetId name } } } } ) ``` By following this approach, you can create tree-like structures and organize accounts in a way that suits your specific use case. Here's an example of a tree structure that can be created using nested account sets: ```mermaid graph BT 1 & 2 & Y --> X 3 --> Y ``` In this example, we have an account set "X" which contains account #1, account #2, and a nested account set "Y" which contains account #3. ## Account Set Operations Use GraphQL queries and mutations to read, create, add members to, update, and delete (lock) account sets: - [`Query.accountSet()`](/docs/reference/graphql/queries#account-set): Get a single account set. - [`Query.accountSets()`](/docs/reference/graphql/queries#account-sets): Query account sets using index filters. - [`Mutation.createAccountSet()`](/docs/reference/graphql/mutations#create-account-set): Create a new account set. - [`Mutation.addToAccountSet()`](/docs/reference/graphql/mutations#add-to-account-set): Add a member (account or account set) to an existing account set. - [`Mutation.removeFromAccountSet()`](/docs/reference/graphql/mutations#remove-from-account-set): Remove a member (account or account set) from an existing account set. - [`Mutation.updateAccountSet()`](/docs/reference/graphql/mutations#update-account-set): Update select fields for an existing account set. - [`Mutation.deleteAccountSet()`](/docs/reference/graphql/mutations#delete-account-set): Soft delete (lock) a specified account set. ## Further Reading To learn how to work with account sets, see the tutorial on [Organizing With Account Sets](/docs/tutorials/organizing-with-account-sets). For more context on how and why Twisp uses account sets, see [Chart Of Accounts](/docs/accounting-core/chart-of-accounts). To review the GraphQL docs for the `AccountSet` type, see [AccountSet](/docs/reference/graphql/types/object#account-set). --- # Accounts Reference for the account resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/accounts ## The Basics Accounts are a named **store of value** that hold a balance, as well as a **record of activity** in the form of ledger entries posted to them. They are an essential part of the Twisp accounting core and are used to model all of the economic activities your ledger provides. Additionally, organizing accounts into sets allows for easy aggregation of balances and querying of ledger entries, enabling you to create a more flexible and powerful structure for your chart of accounts. ## Components of Accounts There are 5 key components defining an account: 1. **Code**: a unique name to identify the account, usually formatted in UPPER_SNAKE_CASE. 2. **Balances**: reflect the net result of all transactions affecting the account. Balances are materialized for each layer, currency, and journal used by entries in the account. 3. **Entries**: the individual journal entries written in transactions posted to the account, providing a detailed history of all financial events that have affected the account over time. 4. **Sets**: list the account sets of which this account is a member. 5. **Normal Balance Type**: determines how the account's normal balances are computed. In addition, accounts have other properties which are common to most or all resources in the accounting core: - **ID**: a universally unique identifier (UUID) for the account. - **Name**: a descriptive name. - **Description**: a free-form text to be used for describing anything about the account. We recommend using this field to summarize what the account is for and when it should be used. - **Metadata**: unstructured, user-specified JSON. Can be combined with custom indexes for powerful querying capacities. - **Config**: allows setting up an account for concurrent posting. - **Created & Updated Timestamps**: self-evident: when the account was created and when it was last updated. - **Version & History**: accounts, like every other record in the accounting core, maintain a list of all changes made to them in their `history` field, and the `version` field indicates the current active version of the account. ## Account Operations Use GraphQL queries and mutations to read, create, update, and delete (lock) accounts: - [`Query.account()`](/docs/reference/graphql/queries#account): Get a single account. - [`Query.accounts()`](/docs/reference/graphql/queries#account-sets): Query accounts using index filters. - [`Mutation.createAccount()`](/docs/reference/graphql/mutations#create-account): Create a new account. - [`Mutation.updateAccount()`](/docs/reference/graphql/mutations#update-account): Update select fields for an existing account. - [`Mutation.deleteAccount()`](/docs/reference/graphql/mutations#delete-account): Soft delete (lock) a specified account. - [`Mutation.addToAccountSet()`](/docs/reference/graphql/mutations#add-to-account-set): Add an account to a set. - [`Mutation.removeFromAccountSet()`](/docs/reference/graphql/mutations#remove-from-account-set): Remove an account from a set. ## Further Reading To learn how to work with accounts, see the tutorial on [Setting Up Accounts](/docs/tutorials/setting-up-accounts). For more context on how and why Twisp uses accounts, see [Chart Of Accounts](/docs/accounting-core/chart-of-accounts). To review the GraphQL docs for the `Account` type, see [Account](/docs/reference/graphql/types/object#account). --- # Balances Reference for the balance resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/balances ## The Basics Balances are auto-calculated sums of the entries for a given account or account set, providing a snapshot a financial position. Balances play a crucial role in accounting, as they provide a snapshot of an account's value at any given time. In the Twisp system, accounts and balances are closely related. Balances in Twisp... - Are automatically calculated as entries are written - Separate debit and credit balances maintained - Are attached to a specific account, journal, and currency - Roll up into normal balances based on account's normal balance type ## Components of a Balance Record - **Account**: determines which account's entries the balance is computed with. - **Entries**: list all the entries used to compute the balance. The most recent entry is also stored as a reference. - **Journal**: is the journal from which entries are pulled to compute the balance. Each account's balance is calculated per journal. - **Currency**: is the currency used by entries in the balance. - **Balance Amounts** for each layer (SETTLED, PENDING, ENCUMBRANCE, and the dynamic AVAILABLE layer), which contain the sums for... - The **debit** balance of the account - The **credit** balance of the account - The **normal** balance, which is calculated difference between credits and debits (for credit-normal accounts) or between debits and credits (for debit-normal accounts). Balance records also maintain `created`, `modified`, and `committed` timestamps as well as their [Versions And History](/docs/reference/ledger/versions-and-history). ## Point-in-time Balances With [point-in-time queries](/docs/reference/ledger/versions-and-history#point-in-time-queries), balance `history` is filterable by timestamp. This provides the ability to retrieve the active balance at a specific time. ## Balance Operations Use GraphQL queries to read balances directly: - [`Query.balance()`](/docs/reference/graphql/queries#balance): Get a single balance. - [`Query.balances()`](/docs/reference/graphql/queries#balances): Query balances using index filters. Balances can also be queried relationally as fields on the [Account](/docs/reference/graphql/types/object#account) and [AccountSet](/docs/reference/graphql/types/object#account-set) objects. ## Further Reading To learn more about querying balances, see the tutorial on [Pulling Balances](/docs/tutorials/pulling-balances). For more context on how balances work, see [Balances](/docs/accounting-core/balances). To review the GraphQL docs for the `Balance` type, see [Balance](/docs/reference/graphql/types/object#balance). --- # Entries Reference for the entry resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/entries ## The Basics Entries are records of a money movement into or out of an account in the Twisp ledger. They represent the individual line items in a ledger journal, storing details such as the account and posting transaction. > **Note:** > > In Twisp, an _entry_ is a fundamental accounting concept representing **one side of a transaction** in a ledger. Transactions are the only way to write to a Twisp ledger. Every transaction posted to a ledger writes at least two entries (one or more debits and one or more credits) which balance to zero, in accordance with double-entry accounting principles. This ensures that the accounting system maintains a high level of integrity and consistency in the ledger record. In other accounting systems, entries might be called "ledger lines" or "journal entries." However, the fundamental concept remains the same. In Twisp, entries always have an account, amount (in units of a currency), direction (`CREDIT` or `DEBIT`), and an entry type that assigns every entry to a categorical type. These entries are the building blocks of a comprehensive, accurate, and reliable financial record. ## Components of a Ledger Entry 1. **Account**: to which the entry is written. This account represents the financial aspect (e.g., asset, liability, expense) impacted by the entry. 2. **Amount**: expressed in numerical (decimal) **units** of a specified **currency** denomination. 3. **Direction**: denotes which side of the ledger the entry is posted on. This is a crucial aspect of double-entry accounting, as every transaction must have at least one entry on the debit side and one on the credit side. 4. **Layer**: on which this entry is recorded (SETTLED, PENDING, or ENCUMBRANCE). 5. **Type**: a categorical label for the entry, such as "TRANSFER_DR" for an entry representing the debit side of a transfer. 6. **Transaction**: references the transaction record in which the entry was written. 7. **Journal**: within which the entry was written. 8. **Sequence**: indicates the order in which the entry was written within the context of the posting transaction. In addition, entries have other properties which are common to most or all resources in the accounting core: - **ID**: a universally unique identifier (UUID) for the entry. - **Name**: a descriptive name. - **Description**: a free-form text to be used for describing anything about the entry. We recommend using this field to summarize what the entry is for and when it should be used. - **Metadata**: unstructured, user-specified JSON. Can be combined with custom indexes for powerful querying capacities. - **Created & Updated Timestamps**: self-evident: when the entry was created and when it was last updated. - **Version & History**: entries, like every other record in the accounting core, maintain a list of all changes made to them in their `history` field, and the `version` field indicates the current active version of the entry. ## Entry Operations Entries cannot be written directly, only indirectly by posting transactions. Entries can be queried using GraphQL: - [`Query.entry()`](/docs/reference/graphql/queries#entry): Get a single entry. - [`Query.entries()`](/docs/reference/graphql/queries#entries): Query entries using index filters. Entries can also be queried relationally as fields on the [Account](/docs/reference/graphql/types/object#account), [AccountSet](/docs/reference/graphql/types/object#account-set), [Balance](/docs/reference/graphql/types/object#balance), and [Transaction](/docs/reference/graphql/types/object#transaction) objects. ## Further Reading To learn how write entries through posting transactions, see the tutorial on [Posting Transactions](/docs/tutorials/posting-transactions). For more context on how and why Twisp a layered accounting model, see [Layered Accounting](/docs/accounting-core/layered-accounting). To review the GraphQL docs for the `Entry` type, see [Entry](/docs/reference/graphql/types/object#entry). --- # Ledger Reference A Twisp ledger is composed of accounts, account sets, balances, entries, journals, transactions, and tran codes. Source: https://www.twisp.com/docs/reference/ledger ## Ledger Resources See each of the sub-pages in this reference for details about the corresponding ledger resource. Each resource is accessible via the [GraphQL API](/docs/reference/graphql). - [Accounts](/docs/reference/ledger/accounts): Accounts store value and record activity. - [Account Sets](/docs/reference/ledger/account-sets): Account sets group and organize accounts, rolling up balances for member accounts. - [Balances](/docs/reference/ledger/balances): Balances calculate sums of ledger entries across layers, journals, accounts, and currencies. - [Entries](/docs/reference/ledger/entries): Entries are records of money movement into or out of accounts. - [Indexes](/docs/reference/ledger/indexes): Indexes enable efficient data access in the ledger. - [KV Store](/docs/reference/ledger/kv): KV records store transactional JSON documents by namespace and key. - [Journals](/docs/reference/ledger/journals): Journals keep your financial transactions organized in separate collections. - [Tran Codes](/docs/reference/ledger/tran-codes): Transaction codes define how ledger entries are written when a transaction is posted - [Transactions](/docs/reference/ledger/transactions): Transactions record all accounting events in the ledger. - [Velocity Controls](/docs/reference/ledger/velocity-controls): Velocity controls regulate the rate that transactions can be posted. ## Axioms Governing the Twisp Ledger _This set of axioms summarizes the key behavior of a Twisp ledger._ - **Accounts** are a named store of value in Twisp, with each account having a balance. - Accounts record activity in the form of ledger entries posted to them. - Accounts support multiple layers and have full support for multiple journals. - Accounts are stored as immutable documents with all changes stored in a versioned history. - **Account sets** are custom groups of accounts that aggregate balances and provide a unified interface into the entries for all accounts. - Account sets can contain other sets, allowing for the modeling of more complex structures for a chart of accounts. - A chart of accounts is a collection of account and account sets used to track money within the system. - Account sets belong to a single journal. - Account sets will materialize balances for entries posted to the account set's journal in accounts that are members of the account set or any of its descendant sets. - Entries can only be posted to accounts, not to account sets. - **Balances** are derived from entries written to the ledger. - Balances are calculated for every account. - Querying balances reflects the current state of accounts in the ledger. - Accounts have a debit balance, credit balance, and normal balance on each layer (SETTLED, PENDING, and ENCUMBRANCE). - Balances are materialized for every combination of account, journal, layer, and currency. - Balance calculations are computed on write, not on read. - **Entries** represent one side of a transaction in the Twisp ledger. - Entries always have an account, an amount (including currency), and a direction (CREDIT or DEBIT). - Entries can only be entered in the context of a transaction. - Every entry is assigned to a layer (SETTLED, PENDING, and ENCUMBRANCE) to differentiate between entries in various stages of a transaction lifecycle. - Posting a transaction will create at least 2 ledger entries. - **Journals** are used to organize transactions within separate "books" in the Twisp ledger. - Every ledger contains a default journal. - Users can create, update, and delete additional journals as needed. - **KV records** store arbitrary JSON documents by `(namespace, key)`. - KV records are versioned and maintain a history of changes. - KV records can be queried by namespace or by custom indexes created on `KV`. - **Transactions** record all accounting events in the Twisp ledger. - The only way to write to a ledger is through a transaction. - Every transaction writes two or more entries to the ledger in standard double-entry accounting practice. - Transactions are structured by the tran code used, ensuring that the ledger is consistent, predictable, and correct. - **Transaction codes** (tran codes) define how ledger entries are written in Twisp. - Tran codes accept input values as parameters and write a multi-entry transaction to the ledger. - Params are key-value parameters specified by the transaction code and are used when posting a transaction with a specified tran code. - All transactions using tran codes are executed atomically, ensuring all-or-nothing commits. - Tran codes provide a centralized interface for defining every type of transactional activity in a system. - **Velocity Controls** Define conditions under which a transaction is allowed to be written in Twisp. - Velocity Controls can apply to Accounts or Account Sets. --- # Indexes Reference for indexes within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/indexes ## The Basics All data retrieval operations in Twisp are executed via indexes. This eliminates issues associated with table scans and dynamic query planning, providing consistent and predictable data access patterns. Indexes in Twisp are designed to support sophisticated application requirements: - **Transactionally Consistent Operations**: All database interactions via indexes are transactionally consistent, ensuring strong consistency. - **Enhanced Partition and Sorting Controls**: Twisp’s schema supports compound keys and multi-field sorting, allowing for precise control over how data is partitioned and ordered. - **Compatibility with All Fields**: Any field within the schema, including those within JSON metadata and List collection types, can be used to create an index. This flexibility allows users to tailor their indexing strategy to the specific needs of their application. - **Filterable with CEL Expressions**: Partial indexes can be created based on conditions specified in CEL (Common Expression Language) expressions, enabling targeted indexing of records that meet certain criteria. ## Types of Indexes Multiple index types are supported in Twisp. Each index type supports a unique set of data access patterns. By selecting the appropriate index type, users can optimize query performance and tailor data access to their specific application requirements within the Twisp system. A number of default indexes are included in the Twisp system. These built-in indexes are managed by Twisp to provide efficient query performance for common financial ledger operations. These indexes guarantee data integrity and are transactionally consistent, making them a reliable choice for most standard query operations. ### Custom Indexes Custom indexes in Twisp are pivotal for optimizing queries and structuring data access patterns specific to your application's needs. Custom indexes can be created for the following record types: - `AccountSet` - `Account` - `Balance` - `Entry` - `KV` - `TranCode` - `Transaction` This capability enables precise control over how data related to these records is indexed and accessed. Custom indexes are transactionally consistent. ### Historical Indexes Historical indexes are specialized indexes that allow querying across all versions of records' histories. Given Twisp’s immutable, append-only data store, any data change results in a new version of a record. Historical indexes enable sophisticated queries that include past record states, which regular indexes do not cover. - **Version Tracking:** Indexes every version of a record, enabling retrieval of historical data states. - **Advanced Query Capabilities:** Supports queries like retrieving account balances at specific points in time or finding record states when particular metadata values were set. ### Search Indexes Search indexes integrates OpenSearch's powerful search capabilities within the Twisp system, providing enhanced performance for text-based queries and structured data retrieval. Leveraging OpenSearch allows for full-text search, advanced filtering, and robust search ranking capabilities, enabling users to harness large datasets efficiently. - **Full-Text Search Capabilities**: OpenSearch indexes support search queries across textual data fields, allowing users to perform complex searches that include fuzzy matching, stemming, and tokenization. - **Rich Filtering Options**: Users can apply filters on various fields, such as numeric ranges, date intervals, or specific terms, making it easier to narrow down search results based on contextual requirements. ## Components of Indexes ### Index Key Fields Indexes in Twisp can be created using both root-level fields and nested fields within documents. This flexibility allows for comprehensive indexing strategies that can cater to complex data structures. For example, you can create custom index keys using: - Root-level fields such as `Account.modified`. - Nested fields within objects, such as fields within the arbitrary JSON `metadata` object in a record. This approach ensures the ability to index any data within the system. ### Index Expressions An index key need not be just a field of the underlying record, but can be a function or expression computed from one or more fields in the record. This feature is useful to obtain fast access to records based on the results of computations. ### Partition Keys When designing custom indexes, it's crucial to consider partitioning strategies to ensure performance and scalability. Partitioning by account is commonly sufficient, but specific workloads may necessitate alternative approaches. Be mindful of read and write operations per partition to prevent throttling, as the database supports a fixed amount of bandwidth and operations per second for each partition. ### Sort Keys For a specific partition key, sort keys allow all data that share that partition key to be sorted and retrieved efficiently based on application requirements. ### Unique Constraints Unique constraints ensure that the data contained in a field, or a group of fields, is unique across all records indexed. ### Partial Indexes Partial indexes can be created based on conditions specified in CEL (Common Expression Language) expressions, enabling targeted indexing of records that meet certain criteria. ### Asynchronous Indexes For indexes that may have a hot partition keys prone to throttling, often seen during high-volume data loads, asynchronous indexes can be used to populate the index via a background process. Asynchronous indexes are eventually consistent and unique constraints are not permitted. #### Built-In Async Indexes Several of the standard built-in indexes use asynchronous (eventually consistent) indexes. Results from these indexes may lag slightly behind recent writes. Some async indexes transparently delegate to a strongly consistent index when an `eq` filter is supplied. | Record Type | Index | Consistency | |---|---|---| | Account | `NAME` | Eventually consistent | | Account | `CODE` | Eventually consistent. Strongly consistent when an `eq` filter is supplied | | Account | `STATUS` | Eventually consistent | | AccountSet | `NAME` | Eventually consistent | | AccountSet | `CODE` | Eventually consistent. Strongly consistent when an `eq` filter is supplied | | AccountSet | `members` (`MEMBER_ID`) | Eventually consistent. Strongly consistent when both `accountSetId` and `memberId` use `eq` filters | ### Sharded Indexes Sharded indexes in Twisp provide a mechanism to enhance write scalability for partitions that experience high write throughput. By splitting a single partition into multiple shards, write operations can be distributed across these shards, thereby increasing the overall write capacity for that partition key. Each shard within a sharded partition acts like a mini-partition, handling a portion of the write load. This is similar to RAID-style disk striping, where data is divided across multiple disks to improve performance. In the context of Twisp indexes, sharding allows the system to handle more writes per second for a given partition key by parallelizing the write operations across the shards. However, this increased write capacity comes at the cost of global sort order within the partition. Since the data is distributed across multiple shards, maintaining a strict sort order across the entire partition becomes challenging. Therefore, while sharded indexes are excellent for scenarios requiring high write throughput, they may not be suitable for use cases that rely heavily on sorted data retrieval across the entire partition. To configure a sharded index, you can specify the `partition_shard_count` parameter when creating or updating an index. This parameter determines the number of shards into which each unique partition key is split. For example, setting `partition_shard_count` to 4 will divide each partition into four shards, potentially allowing for up to four times the write throughput compared to a non-sharded partition. Sharded indexes are particularly useful in scenarios where a single partition key experiences a high volume of write operations, which could otherwise lead to write throttling or performance bottlenecks. By distributing the write load, sharded indexes help maintain system responsiveness and throughput. However, it’s important to carefully consider the impact on read operations and sort order before implementing sharded indexes. ### OpenSearch Schemas For search indexes, Twisp provides full control of the underlying OpenSearch schema, including the selection of fields or expressions to be indexed and their respective data types. ## Zero-Downtime Migrations Twisp's migration infrastructure allows for online creation and modification of indexes with zero downtime. This capability ensures that updates and changes to the data schema can be implemented seamlessly, without disrupting service availability. This system allows re-partitioning of indexes on the fly, ensuring that your system remains responsive and available even during significant changes. ## Index Operations - [`Query.schema.index()`](/docs/reference/graphql/queries#schema.index): Query an Index - [`Query.schema.indexes()`](/docs/reference/graphql/queries#schema.indexes): Query Indexes - [`Mutation.schema.createIndex()`](/docs/reference/graphql/mutations#schema.create-index): Create a Custom Index - [`Mutation.schema.createHistoricalIndex()`](/docs/reference/graphql/mutations#schema.create-historical-index): Create a Historical Index - [`Mutation.schema.createSearchIndex()`](/docs/reference/graphql/mutations#schema.create-search-index): Create a Search Index - [`Mutation.schema.deleteIndex()`](/docs/reference/graphql/mutations#schema.delete-index): Create an Index - [`Mutation.schema.updateSearchIndex()`](/docs/reference/graphql/mutations#schema.update-search-index): Update a Search Index --- # Journals Reference for the journal resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/journals ## The Basics Journals are an essential part of accounting systems, as they provide a way to organize and record transactions within separate "books". > **Note:** > > Put simply, journals help you keep your financial transactions organized in **separate collections**. In Twisp, every ledger instance starts with a **default journal**. You can create additional journals as needed to suit your specific accounting structure. ## Components of Journals 1. **Name**: should be self-descriptive and helps in recognizing the journal's purpose. 2. **Status**: can be either `ACTIVE` or `LOCKED`. Journals with a `LOCKED` status do not allow transactions to be posted to them. 3. **Code**: is an optional unique code for the journal, which can be used as an additional reference. Journals also have **Created & Updated Timestamps** as well as a **Version & History**. ## Use Cases for Multiple Journals Having multiple journals can be beneficial in various scenarios. For example, users may create separate journals for different currencies or product-specific journals. By using multiple journals, you can more effectively organize transactions and maintain a clear understanding of your financial activities. ## Journal Operations Use GraphQL queries and mutations to read, create, update, and delete (lock) journals: - [`Query.journal()`](/docs/reference/graphql/queries#journal): Get a single journal. - [`Query.journals()`](/docs/reference/graphql/queries#journals): Query journals using index filters. - [`Mutation.createJournal()`](/docs/reference/graphql/mutations#create-journal): Create a new journal. - [`Mutation.updateJournal()`](/docs/reference/graphql/mutations#update-journal): Update select fields for an existing journal. - [`Mutation.deleteJournal()`](/docs/reference/graphql/mutations#delete-journal): Soft delete (lock) a specified journal. ## Further Reading To learn the basics of managing journals, see the tutorial on [Working With Journals](/docs/tutorials/working-with-journals). To review the GraphQL docs for the `Journal` type, see [Journal](/docs/reference/graphql/types/object#journal). --- # Key Values Ledger Reference for transactional key/value records in a Twisp ledger. Source: https://www.twisp.com/docs/reference/ledger/kv ## The Basics The Key Values (KV) Ledger provides transactional key/value storage for arbitrary structured documents in the Twisp ledger database. Each record is addressed by a natural key made from `(namespace, key)`, where `namespace` groups related records and `key` identifies one record within that namespace. KV records are useful for application state that should live beside ledger data with the same consistency model, such as feature flags, workflow checkpoints, product configuration, integration state, or lookup documents. They are not a replacement for accounting records: money movement should still be modeled with transactions, entries, balances, and tran codes. Like other Twisp records, KV records are versioned. Creating a record starts at `version` 1. Replacing the record writes a new version, preserves `created`, updates `modified`, and makes the latest version visible through current queries. ## Components of KV Records A KV record has the following fields: 1. **Namespace**: a logical grouping key. Namespaces can be used for broad categories such as `flags`, `integrations`, or `workflow-state`. 2. **Key**: the required unique key within the namespace. The `(namespace, key)` pair identifies the current record. 3. **Description**: an optional text field stored with the record. If omitted in GraphQL input, it is stored as an empty string. 4. **Value**: a strongly-typed `Value` document. Scalars like `UUID`, `Decimal`, `Money`, `Date`, and `Timestamp` round-trip with their type intact rather than being coerced into strings or numbers. See [Strong Typing](#strong-typing) for details. 5. **Conditions**: the CEL conditions (if any) that were required to evaluate to `true` before this version of the record was written. Stored as a map of `{name: expression}` pairs for auditing. 6. **Created and Modified Timestamps**: `created` is preserved across updates, while `modified` changes when a new version is written. 7. **Version and Record ID**: `version` identifies the current record version and `_recordId` identifies the underlying Twisp record. 8. **History**: previous versions are available through the `history` field on `KVValue`. The KV Ledger enforces these limits: | Field | Limit | |-------------------|---------------------------------------------------------------------------| | `namespace` | 512 UTF-8 bytes | | `key` | 512 UTF-8 bytes | | persisted payload | 256 KiB, measured as `description` bytes plus the serialized `value` | > **Note:** > > The default KV index is partitioned by `namespace` and each `namespace` has a throughput cap of 1000 Write Units/Second and 3000 Read Units/Second. ## Writing Values Use `Mutation.kv.put()` to create or replace a record. A put for a new `(namespace, key)` pair creates version 1. A put for an existing pair writes the next version. ```graphql mutation PutKv { kv { put( input: { namespace: "flags" key: "feature-a" description: "Crypto transfer V2 rollout" value: { group: "a", enabled: true, rollout: 25, owner: "risk", code: "CRYPTO_TRANSFER_V2" } } ) { namespace key description value version } } } ``` Every `put` writes a new version. To make a write idempotent or contingent on the record's current state, use `conditions` (see [Conditional Writes](#conditional-writes)). ## Strong Typing KV values are strongly typed. The `value` field is a [`Value`](https://buf.build/twisp/api/docs/8ce263d536164f4ca8ea7311df5a5e6c%3Atwisp.type.v1#twisp.type.v1.Value) document, so scalars like `UUID`, `Decimal`, `Money`, `Date`, and `Timestamp` round-trip with their type intact instead of being coerced into strings or numbers. Reading a stored UUID back yields a UUID, not a string that happens to look like one. The mutation below writes a record whose `value.id` is a `UUID`, then captures the stored `value` into the `$val` variable using the `@export` directive so the same request can feed it back into `evaluate` and confirm that the original UUID identity is preserved end-to-end. (`@export(as: "name")` assigns the decorated field's value to the named query variable, making it available to later operations in the same request.) ```graphql mutation DemonstrateValueTyping( $id: UUID = "106c3332-08a4-4ee3-a7dd-77411882a790" $val: Value = "" ) { kv { put( input: { namespace: "flags" key: "type-roundtrip" value: { id: $id } } ) { value @export(as: "val") } } evaluate( expressions: { val: "document.val.id == uuid('106c3332-08a4-4ee3-a7dd-77411882a790')" } document: { val: $val, id: $id } ) } ``` For the underlying gRPC definitions, see: - [`twisp.type.v1.Value`](https://buf.build/twisp/api/docs/8ce263d536164f4ca8ea7311df5a5e6c%3Atwisp.type.v1#twisp.type.v1.Value): the strongly-typed value used for KV payloads. - [`twisp.core.v1.KVValue`](https://buf.build/twisp/api/docs/main%3Atwisp.core.v1#twisp.core.v1.KVValue): the KV record message. - [`twisp.core.v1.KVService`](https://buf.build/twisp/api/docs/main%3Atwisp.core.v1#twisp.core.v1.KVService): the gRPC interface for reading and writing KV records. ## Updating Values Use `Mutation.kv.update()` to evolve an existing record without sending a full replacement value. An update evaluates a patch against the existing KV `value`: string leaves are CEL expressions, and non-string leaves are literal patch values. Inside the `expressions` input, `document` refers to the current KV record and `value` refers to the currently stored payload under `document.value`. This means `value.rollout + 5` reads the current `document.value.rollout`. The patch is merged with [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) semantics: - Object values merge recursively. - A `null` patch value deletes that key from an object. - Arrays, scalars, and non-object values replace the target value. Updates only apply to existing records. If no record exists for the `(namespace, key)` pair, the mutation fails with `NOT_FOUND`. Pass a `description` on the update input to replace the stored description (an empty string clears it); omit the field to leave the existing description in place. ```graphql mutation UpdateKvPatch { kv { update( input: { namespace: "flags" key: "feature-a" expressions: { rollout: "value.rollout + 5" owner: null metadata: "{'changedBy': 'kv.update', 'tags': ['fixture', 'patch']}" } conditions: { current_rollout: "value.rollout == 50" } } ) { namespace key description value version } } } ``` To rename a record alongside its value patch, pass `description` on the update input: ```graphql mutation RenameKv { kv { update( input: { namespace: "flags" key: "feature-a" description: "renamed flag" expressions: { rollout: "value.rollout + 5" } } ) { namespace key description value version } } } ``` Every `update` writes a new version, even when the expressions evaluate to the current value. Use `conditions` to gate the write on the record's current state (see [Conditional Writes](#conditional-writes)). ## Conditional Writes `put`, `update`, and `delete` accept CEL `conditions` as a map keyed by condition name, where each value is a CEL expression. All conditions must evaluate to `true` before the write is applied, otherwise the request fails with `BAD_REQUEST`. Conditions see the **current** state of the record — the version about to be replaced, updated, or deleted — through the `document` and `value` variables. On successful `put` and `update`, the request's `conditions` are persisted on the written `KVValue` as an audit trail of what was required at write time. > **Note:** > > **`document` vs. `value` in KV conditions** > > Conditions evaluate with both the record header and the payload in scope: > > - **`document`** is the full [`KVValue`](https://buf.build/twisp/api/docs/main%3Atwisp.core.v1#twisp.core.v1.KVValue). Reach for it when you need record-level fields: `document.namespace`, `document.key`, `document.description`, `document.created`, `document.modified`, or the nested `document.value`. > - **`value`** is a shortcut for the payload — equivalent to `document.value`, but it skips a hop. Use it when reading payload fields: `value.rollout`, `value.enabled`, and so on. > > The same split applies anywhere else that evaluates CEL against a KV record, including custom index partitions and sort keys (see [Listing Values](#listing-values)). On `put` and `delete`, when no record exists yet for the `(namespace, key)` pair, both `document` and `value` are `null`. Use `document == null` to gate create-only writes and `document != null` to require that a record already exists. On `update`, the record is always present because `UpdateKv` fails with `NOT_FOUND` before conditions run when the record is missing. This put only succeeds when the record already exists: ```graphql mutation PutIfExists { kv { put( input: { namespace: "flags" key: "feature-a" description: "Crypto transfer V2 rollout" value: { group: "a", enabled: false, rollout: 50, owner: "risk", code: "CRYPTO_TRANSFER_V2" } conditions: { must_exist: "document != null" } } ) { namespace key value version } } } ``` Conditional writes are also useful for create-only writes: ```graphql conditions: { must_not_exist: "document == null" } ``` ## Reading Values Use `Query.kv()` to read the current record by `(namespace, key)`. If no current record exists, the field returns `null`. ```graphql query GetKv { kv(namespace: "flags", key: "feature-a") { namespace key description value version history(first: 10) { nodes { key version value } } } } ``` The `history` connection on `KVValue` returns versions newest-first. ## Listing Values Use `Query.kvs()` to list KV records through an index. The built-in namespace index lists records in one namespace. It requires `where.namespace.eq`. ```graphql query ListKvsByNamespace { kvs( index: { name: Namespace, sort: ASC } where: { namespace: { eq: "flags" } } first: 10 ) { nodes { key description value version } } } ``` Use a custom index when the read pattern is not organized by namespace. Custom KV indexes are created with `on: KV` and queried with `index: { name: Custom }`. ```graphql mutation CreateKvGroupLookupIndex { schema { createIndex( input: { name: "group_lookup" on: KV unique: false partition: [{ alias: "group", value: "string(value.group)" }] sort: [{ alias: "key", value: "string(document.key)", sort: ASC }] constraints: { hasGroup: "has(value.group)" } } ) { name on unique } } } ``` ```graphql query ListKvsByCustomIndex { kvs( index: { name: Custom, sort: ASC } where: { custom: { index: "group_lookup" partition: [{ alias: "group", value: { eq: "a" } }] sort: [{ alias: "key", value: { gte: "" } }] } } first: 10 ) { nodes { key description value version } } } ``` ## Deleting Values Use `Mutation.kv.delete()` to delete the current record for a `(namespace, key)` pair. The mutation returns the deleted record when one existed, otherwise it returns `null`. ```graphql mutation DeleteKv { kv { delete(namespace: "flags", key: "feature-a") { namespace key description value } } } ``` Delete also accepts CEL `conditions` that run against the current record before the delete is applied. `document` and `value` bind to the current record, or to `null` when no record exists — so `document != null` gates a delete on the record already existing, and `document.value.state == 'archived'` gates on a specific payload state. ```graphql mutation DeleteKvIfArchived { kv { delete( namespace: "flags" key: "feature-a" conditions: { only_if_archived: "document.value.state == 'archived'" } ) { namespace key } } } ``` After deletion, `Query.kv()` returns `null` for that `(namespace, key)`, and namespace or custom-index lists no longer include the deleted record. ## Combining KV Operations in One Request Only top-level mutation fields are executed sequentially by GraphQL. The fields *inside* a single `kv { ... }` selection are ordinary object sub-fields and run **in parallel**, so they can't see each other's writes. A `put` followed by an `update` in the same `kv { ... }` block will race: the `update` queries at the same time the `put` runs and sees no record, and the whole transaction aborts with `NOT_FOUND`. To run multiple KV operations against the same record in a single request, give each its own top-level `kv { ... }` selection with a field alias: ```graphql mutation PutThenUpdate { created: kv { put(input: { namespace: "flags", key: "feature-a", value: { rollout: 25 } }) { version } } incremented: kv { update( input: { namespace: "flags" key: "feature-a" expressions: { rollout: "value.rollout + 5" } } ) { version value } } } ``` Because `created` and `incremented` are top-level mutation fields, GraphQL executes them in order — the `put` finishes writing to the shared request transaction before the `update` runs its lookup. Aliased sub-fields inside one `kv { ... }` block are only safe when the operations are independent (different records, or all inserts of distinct keys). ## Real World Examples ### Feature Flag Rollout Because KV records share the same consistency model and read path as the rest of the ledger, they make a natural place to keep small operational knobs that need to be consulted inside the same request as a write. A common pattern is to use a KV record as a feature flag that controls which code a transaction posts under, so a new code can be rolled out gradually without redeploying. The flag itself is a normal KV record. Its `value` carries the rollout percentage and the candidate code, exactly matching the shape used in the [Writing Values](#writing-values) example: ```json { "group": "a", "enabled": true, "rollout": 25, "owner": "risk", "code": "CRYPTO_TRANSFER_V2" } ``` The mutation below reads the flag inside the same request that posts the transaction. The `@export` directive evaluates a CEL expression against the stored `value`: when a uniformly distributed roll falls under `document.rollout`, the new `code` from the flag is exported into `$code`; otherwise the caller-supplied default is kept. The `postTransaction` call then uses whichever code was selected. ```graphql # posts using `CRYPTO_TRANSFER_V1` or `CRYPTO_TRANSFER_V2` based on flag. mutation PostTransactionFromFeatureFlag( $ns: String = "flags" $key: String = "feature-a" $transactionId: UUID = "23872efc-39c6-11f1-a5bd-069b540ea27b" $code: String = "CRYPTO_TRANSFER_V1" $params: JSON = "{}" ) { queries { kv(namespace: $ns, key: $key) { value @export( as: "code" cel: "rand.Intn(100) < document.rollout ? document.code : context.vars.code" ) } } postTransaction( input: { transactionId: $transactionId tranCode: $code params: $params } ) { transactionId } } ``` This pattern works because the KV read, the CEL evaluation, and the transaction post all happen inside one transactional request: there is no window in which the flag could change between the lookup and the post, and the rollout decision is recorded in the same audit trail as the transaction it produced. Updating the rollout percentage or the target code is a single `Mutation.kv.put()` away, and pairing it with the [Conditional Writes](#conditional-writes) pattern keeps concurrent edits to the flag safe. ### Tokenized Card Vault KV records are also useful for storing typed references - like a card token paired with its expiration - under a per-customer namespace. Addressing the record as `customer001.cards:card001` gives the rest of the system a stable handle to look the card up by, without standing up a bespoke table for each kind of reference. Because [`Value` is strongly typed](#strong-typing), the `UUID` token and `Date` expiration are stored and read back as their native types instead of being flattened into strings. The write is an ordinary put. The variables flow straight into `value` with their declared types: ```graphql mutation WriteCardData( $cardNamespace: String! = "customer001.cards" $cardKey: String! = "card001" $cardToken: UUID! = "8f3b2a01-1c4d-4e7a-9b62-2c71f3d44a01" $cardExpiration: Date! = "2026-01-01" ) { kv { put( input: { namespace: $cardNamespace key: $cardKey value: { cardToken: $cardToken cardExpiration: $cardExpiration } } ) { namespace key } } } ``` Reading the card back and using it inside the same request follows the same shape as the [Feature Flag Rollout](#feature-flag-rollout) example. Aliasing the `value` field lets the same selection be exported twice with different `cel` projections, so each typed field lands in its own variable: ```graphql mutation UseCardData( $cardNamespace: String! = "customer001.cards" $cardKey: String! = "card001" $transactionId: UUID! = "8a4ec2ee-9d3e-4f1a-9c82-ab12cd34ef56" $cardToken: UUID $cardExpiration: Date ) { queries { kv(namespace: $cardNamespace, key: $cardKey) { cardToken: value @export(as: "cardToken", cel: "document.cardToken") cardExpiration: value @export(as: "cardExpiration", cel: "document.cardExpiration") } } postTransaction( input: { transactionId: $transactionId tranCode: "CARD_AUTH" params: { cardToken: $cardToken cardExpiration: $cardExpiration } } ) { transactionId } } ``` `$cardToken` and `$cardExpiration` keep their `UUID` and `Date` types end to end — from the stored `value`, through the CEL projection, into the `postTransaction` call — without any string parsing in between. ## KV Operations Use GraphQL queries and mutations to read, write, list, and delete KV records: - [`Query.kv()`](/docs/reference/graphql/queries#kv): Get the current KV record for a `(namespace, key)` pair. - [`Query.kvs()`](/docs/reference/graphql/queries#kvs): List KV records by namespace or custom index. - [`KVValue.history()`](/docs/reference/graphql/types/object#kvvalue): Read the version history for a KV record. - [`Mutation.kv.put()`](/docs/reference/graphql/mutations#kv-put): Create or replace a KV record. - [`Mutation.kv.update()`](/docs/reference/graphql/mutations#kv-update): Evaluate patch expressions and merge them into an existing KV record. - [`Mutation.kv.delete()`](/docs/reference/graphql/mutations#kv-delete): Delete the current KV record for a `(namespace, key)` pair. - [`Mutation.schema.createIndex()`](/docs/reference/graphql/mutations#create-index): Create a custom index on `KV`. ## Further Reading For custom index design, see [Indexes](/docs/reference/ledger/indexes). For record versioning and history, see [Versions And History](/docs/reference/ledger/versions-and-history). To review the GraphQL docs for the `KVValue` type, see [KVValue](/docs/reference/graphql/types/object#kvvalue). --- # Money Formatting Reference for money formatting options for amounts in a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/money-format ## The Basics The [Money](/docs/reference/graphql/types/object#money) type in Twisp represents a monetary amount. Fields of this type can be represented (via its `formatted` field) in various ways depending on the options provided in the [MoneyFormatInput](/docs/reference/graphql/types/input#money-format-input). This reference provides a review of the different ways to format monetary amounts in Twisp, including formatting whole numbers, minimum and maximum digits, rounding modes, locales, grouping, currency minimum digits, and currency displays. > **Note:** > > Unless otherwise indicated, all examples below use the `en-US` locale. ## Minor Units Setting the `minDigits` and `maxDigits` determines how many digits of the minor units to display. When `minDigits` not specified, it will use the default fractional digits for the currency. | Units | Currency | Min Digits | Max Digits | Formatted | |-------------|----------|------------|------------|-------------| | 9 | USD | _DEFAULT_ | _DEFAULT_ | `$9.00` | | 9 | USD | _DEFAULT_ | 0 | `$9` | | 9 | USD | 0 | _DEFAULT_ | `$9` | | 9 | USD | 1 | _DEFAULT_ | `$9.0` | | 9 | USD | 2 | _DEFAULT_ | `$9.00` | | 9 | USD | 3 | _DEFAULT_ | `$9.000` | | 9.123456789 | USD | _DEFAULT_ | _DEFAULT_ | `$9.123457` | | 9.123456789 | USD | _DEFAULT_ | 0 | `$9` | | 9.123456789 | USD | _DEFAULT_ | 0 | `$9.1` | | 9.123456789 | USD | _DEFAULT_ | 0 | `$9.12` | | 9.123456789 | USD | _DEFAULT_ | 0 | `$9.123` | ## Rounding The `roundingMode` option defines the rounding behavior when the fractional units exceed the `maxDigits`. | Units | Currency | Rounding Mode | Max Digits | Formatted | |-------|----------|---------------|------------|-----------| | 9.005 | USD | `DOWN` | 2 | `$9.00` | | 9.005 | USD | `HALF_DOWN` | 2 | `$9.00` | | 9.005 | USD | `UP` | 2 | `$9.01` | | 9.005 | USD | `HALF_UP` | 2 | `$9.01` | | 9.006 | USD | `DOWN` | 2 | `$9.00` | | 9.006 | USD | `HALF_DOWN` | 2 | `$9.01` | | 9.006 | USD | `UP` | 2 | `$9.01` | | 9.006 | USD | `HALF_UP` | 2 | `$9.01` | ## Currency Display Use the `currencyDisplay` to change the currency indicator. | Units | Currency | Currency Display | Formatted | |----------|----------|------------------|----------------| | 12345.67 | USD | _DEFAULT_ | `$12345.67` | | 12345.67 | USD | `CODE` | `USD 12345.67` | | 12345.67 | USD | `NONE` | `12345.67` | | 12345.67 | USD | `SYMBOL` | `$12345.67` | ## Other Locales Changing the `locale` option formats the amount according to the standards of that locale. | Units | Currency | Locale (Country) | Formatted | |---------------------|----------|--------------------|------------------------| | 123456789.123456789 | USD | `zh-CN` (China) | `US$123456789.123457` | | 123456789.123456789 | USD | `es-CO` (Colombia) | `US$ 123456789,123457` | | 123456789.123456789 | USD | `fr-FR` (France) | `123456789,123457 $US` | | 123456789.123456789 | USD | `de-DE` (Germany) | `123456789,123457 $` | | 123456789.123456789 | USD | `hi-IN` (India) | `$123456789.123457` | | 123456789.123456789 | USD | `ja-JP` (Japan) | `$123456789.123457` | | 123456789.123456789 | USD | `ar-AE` (UAE) | `US$ 123456789.123457` | | 123456789.123456789 | USD | `en-GB` (UK) | `US$123456789.123457` | | 123456789.123456789 | USD | `en-US` (USA) | `$123456789.123457` | | 123456789.123456789 | EUR | `zh-CN` (China) | `€123456789.123457` | | 123456789.123456789 | EUR | `es-CO` (Colombia) | `€ 123456789,123457` | | 123456789.123456789 | EUR | `fr-FR` (France) | `123456789,123457 €` | | 123456789.123456789 | EUR | `de-DE` (Germany) | `123456789,123457 €` | | 123456789.123456789 | EUR | `hi-IN` (India) | `€123456789.123457` | | 123456789.123456789 | EUR | `ja-JP` (Japan) | `€123456789.123457` | | 123456789.123456789 | EUR | `ar-AE` (UAE) | `€ 123456789.123457` | | 123456789.123456789 | EUR | `en-GB` (UK) | `€123456789.123457` | | 123456789.123456789 | EUR | `en-US` (USA) | `€123456789.123457` | ## Grouping When `groupDigits` is set to `true`, digits will be grouped according to the standards for each locale. | Units | Currency | Locale (Country) | Formatted | |---------------------|----------|--------------------|--------------------------| | 123456789.123456789 | USD | `zh-CN` (China) | `US$123,456,789.123457` | | 123456789.123456789 | USD | `es-CO` (Colombia) | `US$ 123.456.789,123457` | | 123456789.123456789 | USD | `fr-FR` (France) | `123 456 789,123457 $US` | | 123456789.123456789 | USD | `de-DE` (Germany) | `123.456.789,123457 $` | | 123456789.123456789 | USD | `hi-IN` (India) | `$12,34,56,789.123457` | | 123456789.123456789 | USD | `ja-JP` (Japan) | `$123,456,789.123457` | | 123456789.123456789 | USD | `ar-AE` (UAE) | `US$ 123,456,789.123457` | | 123456789.123456789 | USD | `en-GB` (UK) | `US$123,456,789.123457` | | 123456789.123456789 | USD | `en-US` (USA) | `$123,456,789.123457` | ## Minimum Digits by Currency The default setting for `minDigits` changes depending on which currency is used because different currencies have a different number of minor units used. - US dollars (`USD`) use 2 minor units (i.e. cents). - Jordanian Dinars (`JOD`) use 3 minor units. - Uganda Shillings (`UGX`) use no minor units. `UGX` is the code for the Uganda shilling, which uses 0 minor units. | Units | Currency | Formatted | |-------|----------|-------------| | 9 | USD | `$9.00` | | 9 | JOD | `JOD 9.000` | | 9 | UGX | `UGX 9` | --- # Transaction Codes Reference for the tran code resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/tran-codes ## The Basics Transaction codes (AKA "tran codes") define how ledger entries are written in Twisp. You can think of them as macros or templates for transactions; instead of writing out every transaction by hand within your application layer, you design the tran codes to match every type of transaction in your funds flow. > **Note:** > > In a nutshell, transaction codes **accept input values** and **write a multi-entry transaction to the ledger**. In this way, tran codes form a _self-documenting API for your funds flow_. The benefits of this approach are legion: - Strong separation of concerns between accounting logic (i.e. how ledger entries need to be written to which accounts) and product/business logic. - All transactions are executed atomically. Enforcing all-or-nothing commits leads to correct and easy-to-reason-about systems. - Centralized interface for defining every type of transactional activity that your system handles, providing a rich view into your funds flow. - A linear—not exponential—growth curve in system complexity as more and more transaction types are added to reflect the growth of your financial product. With tran codes as the mechanism for structuring your transactions, you can reign in your funds flow and prevent it from becoming a tangled and unmanageable mess. ## Components of Tran Codes There are 4 primary components which define a tran code: 1. **Code**: a unique name to identify the tran code, usually formatted in UPPER_SNAKE_CASE. When posting a transaction, this is the value supplied to the `tranCode` field to specify which tran code should be invoked. 2. **Params**: the definition of the parameters available when invoking a tran code. This allows values to be supplied at runtime which can be injected into the [Transaction](/docs/reference/graphql/types/object#transaction) and [Entries](/docs/reference/graphql/types/object#entry) written by the tran code. 3. **Transaction**: the specification of values to be used for the [Transaction](/docs/reference/graphql/types/object#transaction) written. Invoking a tran code writes a single transaction into the ledger, and the values used for that transaction are defined either as literals or else are derived from values passed in `params`. 4. **Entries:** the specification of values to be used for the ledger [Entries](/docs/reference/graphql/types/object#entry) written for the transaction when a tran code is invoked. The values used for these entries are defined either as literals or else are derived from values passed in `params`. In addition, tran codes have other properties which are common to all records in the accounting core: - **ID**: a universally unique identifier (UUID) for the tran code. - **Description**: a free-form text to be used for describing anything about the tran code. We recommend using this field to summarize what the tran code is for and when it should be used. - **Created & Updated Timestamps**: self-evident: when the tran code was created and when it was last updated. - **Version & History**: tran codes, like every other record in the accounting core, maintain a list of all changes made to them in their `history` field, and the `version` field indicates the current active version of the tran code. ## Tran Code Invocation Whenever you post a transaction in Twisp, you must supply a tran code to invoke. _There is no way to post transactions outside of a tran code invocation._ In this sense, posting a transaction is how you invoke a tran code. One cannot happen without the other. Posting a transaction requires only three components: a `transactionId` to ensure idempotency for the transaction, the `tranCode` identifier matching the `code` field of the tran code to invoke, and the `params` object to provide parameter values for the tran code. **Request** ```graphql mutation PostACHCredit( $userAcctId: UUID! $amount: String! $effective: String! ) { postTransaction( input: { transactionId: "66fcf002-f815-4608-ad38-8455a46d7f02" tranCode: "ACH_CREDIT" params: { account: $userAcctId, amount: $amount, effective: $effective } } ) { transactionId tranCode { code } effective entries(first: 2) { nodes { amount { money: formatted(as: { locale: "en-US" }) } direction layer account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "66fcf002-f815-4608-ad38-8455a46d7f02", "tranCode": { "code": "ACH_CREDIT" }, "effective": "2022-10-02", "entries": { "nodes": [ { "amount": { "money": "$9.53" }, "direction": "DEBIT", "layer": "SETTLED", "account": { "name": "ACH Settlement" } }, { "amount": { "money": "$9.53" }, "direction": "CREDIT", "layer": "SETTLED", "account": { "name": "Example Acct" } } ] } } } } ``` **Variables** ```json { "amount": "9.53", "effective": "2022-10-02", "userAcctId": "fcb5a92f-cafb-4076-93dd-5df6d759a482" } ``` The above example will write a [Transaction](/docs/reference/graphql/types/object#transaction) and [Entries](/docs/reference/graphql/types/object#entry) as defined in the `ACH_CREDIT` tran code, using the parameters supplied. ## Tran Code Operations Use GraphQL queries and mutations to read, create, update, and delete (lock) tran codes: - [`Query.tranCode()`](/docs/reference/graphql/queries#tran-code): Get a single tran code. - [`Query.tranCodes()`](/docs/reference/graphql/queries#tran-codes): Query tran codes using index filters. - [`Mutation.createTranCode()`](/docs/reference/graphql/mutations#create-tran-code): Create a new transaction code. - [`Mutation.updateTranCode()`](/docs/reference/graphql/mutations#update-tran-code): Update select fields for an existing transaction code. - [`Mutation.deleteTranCode()`](/docs/reference/graphql/mutations#delete-tran-code): Soft delete (lock) a specified transaction code. ## Further Reading To learn how to work with tran codes, see the relevant tutorials on [Building Tran Codes](/docs/tutorials/building-tran-codes) and [Designing Tran Codes](/docs/tutorials/advanced/designing-tran-codes). For more context on how and why Twisp uses tran codes, see [Encoded Transactions](/docs/accounting-core/encoded-transactions). To review the GraphQL docs for the `TranCode` type, see [TranCode](/docs/reference/graphql/types/object#tran-code). --- # Transactions Reference for the transaction resource within a Twisp Ledger. Source: https://www.twisp.com/docs/reference/ledger/transactions ## The Basics Transactions in Twisp record all accounting events in the ledger, utilizing transaction codes to automate and categorize entries. Through tran codes, Twisp automates and categorizes ledger entries, streamlining the accounting process and ensuring consistency. > **Note:** > > Transactions capture all **financial activities** and leverage tran codes to maintain a well-organized and accurate ledger. Transactions in Twisp... - Utilize double-entry accounting principles (ensures that the debit and credit entries balance out) - Ensure atomic, all-or-nothing commits for predictable and error-resistant ledgering - Leverage transaction codes (tran codes) to automate and categorize entries - Maintain a complete history of all changes to records ## Components of Transactions There are 5 primary components which make up a transaction: 1. **Entries**: written to the ledger by this transaction. 2. **Journal**: in which the transaction is posted. 3. **Tran Code**: provides a reference to the tran code used when posting this transaction. 4. **Effective**: date when the transaction is recorded as occurring for accounting purposes. 5. **Correlated**: transactions connect related transactions together, providing context and improving traceability within the ledger. In addition, transactions have other properties which are common to most or all resources in the accounting core: - **ID**: a universally unique identifier (UUID) for the transaction. - **Description**: a free-form text to be used for describing anything about the transaction. - **Metadata**: unstructured, user-specified JSON. Can be combined with custom indexes for powerful querying capacities. - **Created & Updated Timestamps**: self-evident: when the transaction was created and when it was last updated. - **Version & History**: transactions, like every other record in the accounting core, maintain a list of all changes made to them in their `history` field, and the `version` field indicates the current active version of the transaction. ## Transaction Operations Use GraphQL to query, post, and update transactions: - [`Query.transaction()`](/docs/reference/graphql/queries#transaction): Get a single transaction. - [`Query.transactions()`](/docs/reference/graphql/queries#transactions): Query transactions using index filters. - [`Mutation.postTransaction()`](/docs/reference/graphql/mutations#post-transaction): Post a transaction to the ledger using a specified tran code. - [`Mutation.updateTransaction()`](/docs/reference/graphql/mutations#update-transaction): Update select fields for a posted transaction. ## Further Reading To learn the basics of posting transactions, see the tutorial on [Posting Transactions](/docs/tutorials/posting-transactions). For more context on why Twisp structures transactions with tran codes, see [Encoded Transactions](/docs/accounting-core/encoded-transactions). To review the GraphQL docs for the `Transaction` type, see [Transaction](/docs/reference/graphql/types/object#transaction). --- # Velocity Controls Reference for the Velocity Control resources within a Twisp ledger. Source: https://www.twisp.com/docs/reference/ledger/velocity-controls ## The Basics Velocity Controls in the Twisp Accounting Core provide a mechanism to define and enforce restrictions on the flow of transactions and balances within accounts and account sets. These controls are essential for managing financial governance, ensuring compliance, and mitigating risks by setting specific limits on account activities. ## Components of a Velocity Control A Velocity Control is made up of two main components: - **Velocity Control**: Defines the enforcement actions when one or more Velocity Limits are violated. - **Velocity Limit**: Defines a balance and a parameterizable limit that is enforced. This balance can occur across a number of dimensions. Once a Velocity Control is defined, it may be **attached** to accounts and/or account sets to begin enforcing the defined limits for those accounts or sets. If during the posting of a transaction, an attached Velocity Control is violated an exception will be generated and depending on the enforcement configuration, the transaction will be rejected, voided or warned. ### Velocity Control There are 3 key components that make up a Velocity Control 1. **Enforcement**: A Velocity Control can define it's enforcement mechanism, there are three `WARN`, `VOID` and `REJECT`. This defines whether the violated entry is written to the ledger and how the ledger will respond to the violating entry. 2. **Velocity Limits**: The Velocity Limits on a control define one or more balances that are under enforcement of a particular control. These balances can contain windowing and filtering criteria to keep track of different types of balances that will be enforced on by the Velocity Control. 3. **Condition**: A CEL expression which evaluates to a boolean value allows for fine grained control to determine if the Velocity Control will execute enforcement. For example, a Velocity Limit may be exceeded, but the control only enforces `PENDING` transactions. ### Velocity Limits Velocity Limits form the foundation of Velocity Controls. They specify the maximum allowable balance or transaction volume over a defined period. Each Velocity Limit is tailored to control spending, deposits, or any financial activity based on parameters like amount, currency, and account layers (SETTLED, PENDING, ENCUMBRANCE). By enforcing these limits, organizations ensure that financial activities remain within predefined thresholds, maintaining financial discipline and control. There are 5 key components of a Velocity Limit: 1. **Window**: Every Velocity Limit has a default set of dimensions (currency, journal, account) and this `window` can define additional dimensions, such as bucketed time periods or values from account or entry metadata. 2. **Limit**: The limit to apply, which currently only supports an available balance limit. This defines the layer, amount, and normal balance type that the limit supports. 3. **Condition**: A CEL expression which evaluates to a boolean value for filtering if an entry is eligible to apply to the limit. For example if a limit is only applied to a certain category of transactions defined in transaction metadata. 4. **Currency**: Defines which currency the limit applies to. 5. **Params**: The definition of the parameters available when attaching a a limit used in a Velocity Control to an account. This allows values to supplied at attachment time so that a Velocity Control can have different enforced limits for different accounts. ### Attaching Velocity Controls Attaching a Velocity Control to an Account or Set enables that Account or Set to begin enforcing entries that are posted. There are three components to an attachment: 1. **Velocity Control Id**: Identifies the particular Velocity Control to attach. 2. **Account or Set Id**: The account or set to attach control to. 3. **Params**: A JSON set of parameters that satisfies all the defined parameters on the limits attached to the controls. > **Note:** > > When defining parameters on Velocity Limits use _unique parameter names_ to avoid collisions between parameters. ### Overriding Velocity Control Enforcement Velocity Control enforcement can be escalated or de-escalated for a control with an action configured at the `VOID` or `REJECT` level, by passing the `overrideVelocityEnforcement` parameter on [postTransaction](/docs/reference/graphql/mutations#post-transaction). This is useful for setting to `WARN` for force post transactions that require disabled velocity control enforcement. ## Velocity Control Operations Use GraphQL queries and mutations to read, create, delete and attach Velocity Controls and limits: - [`Query.velocityControl()`](/docs/reference/graphql/queries#velocity-control): Get a single Velocity Control - [`Query.velocityControls()`](/docs/reference/graphql/queries#velocity-controls): Query Velocity Controls - [`Query.velocityLimit()`](/docs/reference/graphql/queries#velocity-limit): Get a single Velocity Limit - [`Query.velocityLimits()`](/docs/reference/graphql/queries#velocity-limits): Query Velocity Limits - [`Mutation.createVelocityControl()`](/docs/reference/graphql/mutations#create-velocity-control): Create a Velocity Control - [`Mutation.updateVelocityControl()`](/docs/reference/graphql/mutations#update-velocity-control): Update a Velocity Control - [`Mutation.deleteVelocityControl()`](/docs/reference/graphql/mutations#delete-velocity-control): Delete a Velocity Control - [`Mutation.createVelocityLimit()`](/docs/reference/graphql/mutations#create-velocity-limit): Create a Velocity Limit - [`Mutation.updateVelocityLimit()`](/docs/reference/graphql/mutations#update-velocity-limit): Update a Velocity Limit - [`Mutation.deleteVelocityLimit()`](/docs/reference/graphql/mutations#delete-velocity-limit): Delete a Velocity Limit - [`Mutation.attachControl()`](/docs/reference/graphql/mutations#attach-velocity-control): Attach Velocity Control to an Account or Account Set - [`Mutation.detachControl()`](/docs/reference/graphql/mutations#detach-velocity-control): Detach Velocity Control from Account or Account Set --- # Versions and History Every record in a Twisp ledger maintains a full history of all changes applied to it. All previous versions of a record can be queried. Source: https://www.twisp.com/docs/reference/ledger/versions-and-history ## The Basics Record versioning and history is the foundation of [append-only immutability](/docs/infrastructure/ledger-database#append-only-immutability) in the Twisp Accounting Core. All data entering the Twisp system is written as a new data record that cannot be modified. Prohibiting modification or deletion of data ensures a complete log of all changes to the system, allowing for auditable data history. > **Note:** > > Append-only immutability is the only data storage model in the Twisp Accounting Core and cannot be disabled. ## Record Versioning Twisp stores all data within records in the storage system. Each record is assigned a unique record ID. The indexing system provides pointers to records for specific keys. For example, an `accountId` index points to the specific corresponding `account` record. The first version of a new record is assigned `version` 1. When any operation needs to modify that record, a second record is written with the same unique record ID, but with `version` now set to 2. This pattern repeats indefinitely for every record in the system. > **Note:** > > For a particular record, version numbers form a consecutive integer sequence. Each new version number is exactly one greater than the previous version number. In addition to the `version` number, each record version contains a number of useful timestamps: 1. `created`: the time the transaction began that created the first record version. 2. `modified`: the time the transaction began that wrote the record version. If version is 1, `modified` is equal to the `created` timestamp. 3. `committed`: the time the transaction committed that wrote the record version. ## Version History All record versions are queryable via the `history` API available on every object in the system. This allows for retrospective analysis, auditing, and verification of data changes over time. ## Point-in-time Queries In addition to scanning an entire record version history, Twisp provides the ability to return versions active at specific point-in-time. By specifying timestamps in the `history` API `where` filter, version history can be filtered by either the `modified` or `committed` record version timestamps. **Request** ```graphql query GetPointInTimeHistory( $journalGLId: UUID! $accountCardSettlementId: UUID! ) { balance( accountId: $accountCardSettlementId journalId: $journalGLId currency: "USD" ) { history( first: 1 where: { modified: { lt: "2030-04-05T17:45:55.347145Z" } } ) { nodes { version available(layer: SETTLED) { normalBalance { formatted(as: { locale: "en-US" }) } } } } } } ``` **Response** ```json { "data": { "balance": { "history": { "nodes": [ { "version": 3, "available": { "normalBalance": { "formatted": "-$4.53" } } } ] } } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` --- # Warehouse Reference for the data warehouse of Twisp ledgers. Source: https://www.twisp.com/docs/reference/ledger/warehouse ## The Basics The Twisp ledger exports all data to AWS Redshift and exposes APIs to interact with the data using arbitrary SQL queries. When enabled on your tenant, this warehouse provides a convenient way to execute analytical queries or unload data to your own data lake or warehouse. > **Note:** > > **Tip:** If you need to export data in bulk, you can use the [export API](/docs/tutorials/advanced/export-data) without having warehouse enabled. ## Components of Warehouse ### GraphQL Interface Twisp exposes the [Redshift Data API](https://docs.aws.amazon.com/redshift-data/latest/APIReference/Welcome.html) in GraphQL format for execution of SQL queries. The most commonly used API interactions are described here. #### Execute Statement The execute statement API allows you to execute a statement on Redshift. ```graphql mutation ExecuteStatement($SQL:String! = "SELECT TRUE") { warehouse{ executeStatement(input:{ SQL: $SQL }) { id } } } ``` #### Describe Statement Describe Statement allows you to check on the status of a running query and retrieve metadata about it's results. ```graphql query DescribeStatement($id:String!) { warehouse{ describeStatement(input:{ id:$id }) { resultRows resultSize status error } } } ``` #### Cancel Statement Cancel statement cancels a running statement. ```graphql mutation CancelStatement($id:String!) { warehouse{ cancelStatement( input:{ id } ) } } ``` #### Get Statement Result Get Statement Result allows you to fetch the results of a query via the API with paging. If the result is particularly large, you may opt to instead use the [UNLOAD capabilty](https://docs.aws.amazon.com/redshift/latest/dg/r_UNLOAD.html) to export data in a suitable format to an S3 bucket of your choosing. ```graphql query GetStatementResult($id:String!) { warehouse{ getStatementResult(input:{ id:$id #nextToken:"" }) { records { fields { type value { str bytes isNull } } } nextToken } } } ``` ### Views and Schemas Twisp maintains two views for each entity in the ledger: - **`entity`**: the latest version of any entity stored in the Twisp ledger. - **`entity_history`**: all versions of a particular record stored in the Twisp ledger. Each record contains a number of header rows prepended with `record_`: - `record_begin` - A unique timestamp of when the Twisp database transaction began. - `record_rowid` - A uuid identifier of the particular item in Twisp. - `record_status` - An enumeration indicating whether this record is deleted. - `record_tenantid` - A uuid indicating the tenant that this record belongs to. - `record_version` - An monotonically increasing unsigned integer indicating the version of the record. These columns allow for incremental exports via `record_begin`, which are guaranteed to be unique per transaction. In other words, records with the same timestamp originated within the same transaction. The `record_rowid` and `record_version` together uniquely identify a particular version of a record and is a convenient way to de-duplicate rows in the event you use overlapping timestamps to import data. The following entities are available in the warehouse: - `public.account` - `public.account_history` - `public.account_set` - `public.account_set_history` - `public.account_set_member` - `public.account_set_member_history` - `public.balance` - `public.balance_history` - `public.calculation` - `public.calculation_history` - `public.entry` - `public.entry_history` - `public.journal` - `public.journal_history` - `public.tran_code` - `public.tran_code_history` - `public.transaction` - `public.transaction_history` - `public.transaction_exception` - `public.transaction_exception_history` - `public.workflow_execution` - `public.workflow_execution_history` The schemas between each `entity` and `entity_history` view are identical and are provided below. #### account Every Account and Account Set in Twisp has an account record stored. | Column Name | Type | |----------------------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `account_id`| `VARCHAR(36)`| | `status`| `VARCHAR`| | `name`| `SUPER`| | `code`| `VARCHAR`| | `normal_balance_type`| `VARCHAR`| | `description`| `SUPER`| | `metadata`| `SUPER`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `external_id`| `VARCHAR`| | `config_enable_concurrent_posting`| `BOOLEAN`| | `config_is_account_set`| `BOOLEAN`| #### account_set The `account_set` table has all Account Sets in the Twisp ledger. | Column Name | Type| |----------------------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `account_set_id`| `VARCHAR(36)`| | `journal_id`| `VARCHAR(36)`| | `account_id`| `VARCHAR(36)`| | `name`| `VARCHAR`| | `description`| `VARCHAR`| | `metadata`| `SUPER`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `config_enable_concurrent_posting`| `BOOLEAN`| #### account_set_member The `account_set_member` table maintains the Account Set tree membership details. The `member_id` can refer to _either_ an Account Set or Account, based on `member_type`. | Column Name | Type| |-----------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `account_set_id`| `VARCHAR(36)`| | `journal_id`| `VARCHAR(36)`| | `member_type`| `VARCHAR`| | `member_id`| `VARCHAR(36)`| | `created`| `TIMESTAMP`| #### balance Contains all Balance records and versions. Note that the `dimension` column is of `VARBYTE` type. If unloading, you must convert into a base64 encoded string: ```SQL SELECT FROM_VARBYTE(dimension,'base64') AS dimension FROM balance; ``` | Column Name | Type| |----------------------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `journal_id`| `VARCHAR(36)`| | `account_id`| `VARCHAR(36)`| | `transaction_id`| `VARCHAR(36)`| | `entry_id`| `VARCHAR(36)`| | `currency`| `VARCHAR`| | `settled_dr_balance`| `VARCHAR`| | `settled_cr_balance`| `VARCHAR`| | `settled_entry_id`| `VARCHAR(36)`| | `settled_modified`| `TIMESTAMP`| | `pending_dr_balance`| `VARCHAR`| | `pending_cr_balance`| `VARCHAR`| | `pending_entry_id`| `VARCHAR(36)`| | `pending_modified`| `TIMESTAMP`| | `encumbrance_dr_balance`| `VARCHAR`| | `encumbrance_cr_balance`| `VARCHAR`| | `encumbrance_entry_id`| `VARCHAR(36)`| | `encumbrance_modified`| `TIMESTAMP`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `available_settled_dr_balance`| `VARCHAR`| | `available_settled_cr_balance`| `VARCHAR`| | `available_settled_entry_id`| `VARCHAR(36)`| | `available_settled_modified`| `TIMESTAMP`| | `available_pending_dr_balance`| `VARCHAR`| | `available_pending_cr_balance`| `VARCHAR`| | `available_pending_entry_id`| `VARCHAR(36)`| | `available_pending_modified`| `TIMESTAMP`| | `available_encumbrance_dr_balance`| `VARCHAR`| | `available_encumbrance_cr_balance`| `VARCHAR`| | `available_encumbrance_entry_id`| `VARCHAR(36)`| | `available_encumbrance_modified`| `TIMESTAMP`| | `calculation_id`| `VARCHAR(36)`| | `dimension`| `VARBYTE`| | `dimensions`| `SUPER`| | `entry_committed`| `TIMESTAMP`| | `entry_timestamps`| `SUPER`| #### calculation Contains the calculation definitions in the Ledger. | Column Name | Type| |------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `calculation_id`| `VARCHAR(36)`| | `description`| `VARCHAR`| | `code`| `VARCHAR`| | `dimensions`| `SUPER`| | `status`| `VARCHAR`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `condition`| `SUPER`| | `skip_calculation`| `BOOLEAN`| | `backfill_status`| `VARCHAR`| #### entry All of the journal entries in the Ledger. | Column Name | Type| |------------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `entry_id`| `VARCHAR(36)`| | `transaction_id`| `VARCHAR(36)`| | `transaction_seq`| `BIGINT`| | `journal_id`| `VARCHAR(36)`| | `account_id`| `VARCHAR(36)`| | `entry_type`| `VARCHAR`| | `layer`| `VARCHAR`| | `direction`| `VARCHAR`| | `description`| `VARCHAR`| | `amount`| `VARCHAR`| | `balance_record_id`| `VARCHAR(36)`| | `balance_record_version`| `BIGINT`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `metadata`| `SUPER`| | `committed`| `TIMESTAMP`| | `parent_account_ids`| `SUPER`| | `is_voided_entry`| `BOOLEAN`| | `is_void_entry`| `BOOLEAN`| #### journal All of the journals in the Ledger. | Column Name | Type| |----------------------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `journal_id`| `VARCHAR(36)`| | `name`| `VARCHAR`| | `description`| `VARCHAR`| | `status`| `VARCHAR`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `code`| `VARCHAR`| | `config_enable_effective_balances`| `BOOLEAN`| #### tran_code All of the Transaction Code definitions in the Ledger. | Column Name | Type| |----------------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `tran_code_id`| `VARCHAR(36)`| | `code`| `VARCHAR`| | `description`| `VARCHAR`| | `status`| `VARCHAR`| | `params`| `SUPER`| | `transaction_journal_id`| `SUPER`| | `transaction_correlation_id`| `SUPER`| | `transaction_external_id`| `SUPER`| | `transaction_effective`| `SUPER`| | `transaction_description`| `SUPER`| | `transaction_metadata`| `SUPER`| | `entries`| `SUPER`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `metadata`| `SUPER`| #### transaction All of the Transactions posted in the Ledger. | Column Name | Type| |-------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `transaction_id`| `VARCHAR(36)`| | `tran_code_id`| `VARCHAR(36)`| | `journal_id`| `VARCHAR(36)`| | `correlation_id`| `VARCHAR`| | `external_id`| `VARCHAR`| | `effective`| `DATE`| | `description`| `VARCHAR`| | `metadata`| `SUPER`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `tran_code_version`| `BIGINT`| | `void_of`| `VARCHAR(36)`| | `voided_by`| `VARCHAR(36)`| #### transaction_exception All Transaction Exceptions for `WARN` and `VOID` velocity control enforcements. | Column Name | Type| |-----------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `transaction_id`| `VARCHAR(36)`| | `type`| `VARCHAR`| | `error_message`| `VARCHAR`| | `detail`| `SUPER`| | `created`| `TIMESTAMP`| #### velocity_control All Velocity Controls defined in the ledger. | Column Name | Type| |---------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `velocity_control_id`| `VARCHAR(36)`| | `name`| `VARCHAR`| | `description`| `VARCHAR`| | `enforcement`| `SUPER`| | `condition`| `VARCHAR`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| #### velocity_limit All Velocity Limits defined in the Ledger. | Column Name | Type| |-------------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `velocity_limit_id`| `VARCHAR(36)`| | `name`| `VARCHAR`| | `description`| `VARCHAR`| | `window`| `SUPER`| | `condition`| `SUPER`| | `currency`| `VARCHAR`| | `timestamp_source`| `VARCHAR`| | `limit`| `SUPER`| | `params`| `SUPER`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| #### workflow_execution A log of all workflow executions for `workflow.execute`. | Column Name | Type| |-----------------|-------------| | `record_begin`| `TIMESTAMP`| | `record_rowid`| `VARCHAR(36)`| | `record_status`| `VARCHAR`| | `record_tenantid`| `VARCHAR(36)`| | `record_version`| `BIGINT`| | `workflow_id`| `VARCHAR(36)`| | `execution_id`| `VARCHAR(36)`| | `task`| `VARCHAR`| | `params`| `SUPER`| | `context`| `SUPER`| | `created`| `TIMESTAMP`| | `modified`| `TIMESTAMP`| | `output`| `SUPER`| ## Warehouse Operations Use GraphQL queries and mutations to execute queries and describe results in the data warehouse: - [`Query.warehouse.describeStatement()`](/docs/reference/graphql/queries#warehouse.describe-statement): Describe the status of executing query - [`Query.warehouse.describeTable()`](/docs/reference/graphql/queries#warehouse.describe-table): Describe a table. - [`Query.warehouse.getStatementResult()`](/docs/reference/graphql/queries#warehouse.get-statement-result): Retrieve the result of a table. - [`Query.warehouse.listDatabases()`](/docs/reference/graphql/queries#warehouse.list-databases): List databases available to query. - [`Query.warehouse.listSchemas()`](/docs/reference/graphql/queries#warehouse.list-schemas): List schemas available in database. - [`Query.warehouse.listStatements()`](/docs/reference/graphql/queries#warehouse.list-statements): List recent executing statements. - [`Query.warehouse.listTables()`](/docs/reference/graphql/queries#warehouse.list-tables): List tables in schema. - [`Mutation.warehouse.executeStatementSync()`](/docs/reference/graphql/mutations#warehouse.execute-statement-sync): Execute a SQL statement and synchronously wait for result. - [`Mutation.warehouse.batchExecuteStatement()`](/docs/reference/graphql/mutations#warehouse.batch-execute-statement): Execute a number of SQL statements asynchronously. - [`Mutation.warehouse.cancelStatement()`](/docs/reference/graphql/mutations#warehouse.cancel-statement): Cancel a running statement. - [`Mutation.warehouse.executeStatement()`](/docs/reference/graphql/mutations#warehouse.execute-statement): Execute a single SQL statement asynchronously. --- # ACH RDFI Reference for the ACH RDFI processor for a Twisp Ledger. Source: https://www.twisp.com/docs/reference/protocols/ach/rdfi ## The Basics The ACH RDFI processor enables management of ACH transactions within the Twisp ledger system from the perspective of a Receiving Depository Financial Institution (RDFI). It provides endpoints for receiving ACH files, handling transaction postings, processing returns, managing reversals, and integrating these activities into the financial ledger. The Automated Clearing House (ACH) network facilitates electronic financial transactions. An RDFI is responsible for receiving and processing incoming transactions, ensuring they are appropriately credited to beneficiary accounts while maintaining compliance with ACH rules and standards. This processor supports streamlined transaction management and regulatory adherence. ```mermaid graph TD A(Create file upload) --> B(Upload ACH file) B --> C(Process ACH file) C --> D{Decision ACH transaction
Webhook} D -->|Approve| E(Post to ledger account) D -->|Return with reason| F(Generate ACH return) D -->|Retry| D G(Update ACH transaction status) E --> G F --> G G --> H{More transactions in file?} H -->|Yes| D H -->|No| I(Update ACH file status) I --> J(Read ACH file status) ``` ## ACH RDFI Workflow ### 0. Prerequisite ACH RDFI Config An ACH config contains information for processing and generating ACH files. You will need to create an endpoint for responding to processing requests: ```graphql mutation CreateEndpoint { events { createEndpoint( input: { endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" status: ENABLED endpointType: ACH_PROCESSOR url: "https://webhook.site/ach-testing" subscription: [] description: "ACH webhook processor" } ) { endpointId } } } ``` Create the required accounts for processing ach transactions: ```graphql mutation CreateAccounts { # ACH Settlement account settlement: createAccount( input:{ accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" code: "settlement.ach" name: "ach settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId } # Suspense/Exception Account suspenseAndException: createAccount( input:{ accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" code: "suspense.ach" name: "ach suspense" config: { enableConcurrentPosting: true } } ) { accountId } } ``` And then create a configuration that specifies the various accounts required for processing ACH files. **Request** ```graphql mutation CreateConfiguration { ach { createConfiguration( input: { configId: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" exceptionAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" feeAccountId: "62808a87-0b11-4dce-8877-767b70f029af" journalId: "00000000-0000-0000-0000-000000000000" odfiHeaderConfiguration: { immediateDestination: "026009593" immediateDestinationName: "ACME BANK" immediateOrigin: "111000173" immediateOriginName: "ZUZU" } timeZone: "America/Los_Angeles" } ) { configId } } } ``` **Response** ```json { "data": { "ach": { "createConfiguration": { "configId": "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" } } } } ``` #### Accounts 1. `settlementAccountId` is the account that represents entries as they transit the system to end user or exception/expense accounts. 2. `exceptionAccountId` is the account that's posted to when a transaction fails to post to a customer account due to velocity controls, account state or some unknown reason. 3. `suspenseAccountId` is the account that's posted to when the desired account does not exist. ### 1. Upload ACH file All RDFI ACH files received from the network must be uploaded. [`Mutation.files.createUpload()`](#) uploads a new ACH file containing transactions for processing. ```graphql mutation CreateAchUpload{ files { createUpload( input:{ key: "nacha_file.ach" uploadType: ACH contentType: "text/plain" } ) { uploadURL } } } ``` Example Upload of file with `curl`: ``` curl -T nacha_file.ach -XPUT '' ``` ### 2. Process ACH file After a file is uploaded, [`Mutation.ach.processFile()`](/docs/reference/graphql/mutations#ach.process-file) will begin processing the uploaded ACH file. **Request** ```graphql mutation ProcessFile { ach { processFile( input: { configId: "1dc71d60-f463-4bb6-b82a-ab42e2f923ff" fileKey: "ppd-credit.ach" fileType: RDFI } ) { fileId } } } ``` **Response** ```json { "data": { "ach": { "processFile": { "fileId": "b0d76a9a-afcd-4b1b-b115-01b88d7b84e6" } } } } ``` ### 3. Respond to ACH RDFI transaction webhooks Webhooks will be triggered for all entries in the processed ACH file. 1. From the ACH entry details determine the `journalId` and `accountId` the transaction should be posted to. 1. Run any customer defined transaction checks to determine if a return is warranted. 1. Return a response for how Twisp should proceed in processing the transaction along with the timestamp for settlement. > **Tip:** > > #### Interaction with velocity limits > > Something about how velocity limits are the end-all be-all of balance checking. > > Learn more about velocity limits in ... #### Sample webhooks ```json { "workflowName": "ACH.RDFI.DR", "workflowTask": "CREATE", "executionId": "60f7ac42-ff72-48c7-af58-ee1f9a2db1e0", "fileHeader": { "id": "", "immediateDestination": "", "immediateOrigin": "", "fileCreationDate": "", "fileCreationTime": "", "fileIDModifier": "", "immediateDestinationName": "", "immediateOriginName": "", "referenceCode": "" }, "batchHeader": { "id": "", "serviceClassCode": "", "companyName": "", "companyDiscretionaryData": "", "companyIdentification": "", "standardEntryClassCode": "", "companyEntryDescription": "", "companyDescriptiveDate": "", "effectiveEntryDate": "", "settlementDate": "", "originatorStatusCode": "", "odfiIdentification": "", "batchNumber": "" }, "entryDetail": { "id": "", "transactionCode": "", "rdfiIdentification": "", "checkDigit": "", "dfiAccountNumber": "", "amount": "", "identificationNumber": "", "individualName": "", "discretionaryData": "", "addendaRecordIndicator": "", "traceNumber": "", "addenda02": {}, "addenda05": {}, "addenda98": {}, "addenda98Refused": {}, "addenda99": {}, "addenda99Contested": {}, "addenda99Dishonored": {}, "category": "" } } ``` ```json { "action": "SETTLE | RETURN | RETRY", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "when": "2024-01-01T23:20:50Z", "addenda99": { "returnCode": "R01", "dateOfDeath": "", "addendaInformation": "" }, "metadata": { "key": "value" } } ``` ##### Example Responses Where `now` is `2000-02-01T00:00:00.000Z` Settle Now: ```json { "action": "SETTLE", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "when": "2000-01-31T23:59:59.000Z", "metadata": { "key": "value" } } ``` Settle in two days: ```json { "action": "SETTLE", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "when": "2000-02-03T00:00:00.000Z", "metadata": { "key": "value" } } ``` Settle based on settlement date defined in batch: ```json { "action": "SETTLE", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "metadata": { "key": "value" } } ``` Retry Twisp will exponentially backoff: ```json { "action": "RETRY" } ``` Decline with insufficient funds return code: ```json { "action": "RETURN", "accountId": "d2f7183f-8e9c-45e7-9a98-ef1897ddb930", "addenda99": { "returnCode": "R01" }, "metadata": { "key": "value" } } ``` #### Return codes | Code | Reason | Description | |----|-----|------| | `R01` | Insufficient Funds | Available balance is not sufficient to cover the dollar value of the debit entry | | `R02` | Account Closed | Previously active account has been closed by customer or RDFI | | `R03` | No Account/Unable to Locate Account | Account number structure is valid and passes editing process, but does not correspond to individual or is not an open account | | `R04` | Invalid Account Number | Account number structure not valid; entry may fail check digit validation or may contain an incorrect number of digits. | | `R05` | Improper Debit to Consumer Account | A CCD, CTX, or CBR debit entry was transmitted to a Consumer Account of the Receiver and was not authorized by the Receiver | | `R06` | Returned per ODFI's Request | ODFI has requested RDFI to return the ACH entry (optional to RDFI - ODFI indemnifies RDFI) | | `R07` | Authorization Revoked by Customer | Consumer, who previously authorized ACH payment, has revoked authorization from Originator (must be returned no later than 60 days from settlement date and customer must sign affidavit) | | `R08` | Payment Stopped | Receiver of a recurring debit transaction has stopped payment to a specific ACH debit. RDFI should verify the Receiver's intent when a request for stop payment is made to insure this is not intended to be a revocation of authorization | | `R09` | Uncollected Funds | Sufficient book or ledger balance exists to satisfy dollar value of the transaction, but the dollar value of transaction is in process of collection (i.e., uncollected checks) or cash reserve balance below dollar value of the debit entry. | | `R10` | Customer Advises Originator is Not Known to Receiver and/or Originator is Not Authorized by Receiver to Debit Receiver’s Account | The receiver does not know the Originator’s identity and/or has not authorized the Originator to debit. Alternatively, for ARC, BOC, and POP entries, the signature is not authentic or authorized. | | `R11` | Customer Advises Entry Not in Accordance with the Terms of the Authorization | The Originator and Receiver have a relationship, and an authorization to debit exists, but there is an error or defect in the payment such that the entry does not conform to the terms of the authorization. The Originator may correct the error and submit a new entry within 60 days of the return entry settlement date without the need for re-authorization by the Receiver. | | `R12` | Branch Sold to Another DFI | Financial institution receives entry destined for an account at a branch that has been sold to another financial institution. | | `R13` | RDFI not qualified to participate | Financial institution does not receive commercial ACH entries | | `R14` | Representative payee deceased or unable to continue in that capacity | The representative payee authorized to accept entries on behalf of a beneficiary is either deceased or unable to continue in that capacity | | `R15` | Beneficiary or bank account holder | (Other than representative payee) deceased* - (1) the beneficiary entitled to payments is deceased or (2) the bank account holder other than a representative payee is deceased | | `R16` | Bank account frozen | Funds in bank account are unavailable due to action by RDFI or legal order | | `R17` | File record edit criteria | Fields rejected by RDFI processing (identified in return addenda) | | `R18` | Improper effective entry date | Entries have been presented prior to the first available processing window for the effective date. | | `R19` | Amount field error | Improper formatting of the amount field | | `R20` | Non-payment bank account | Entry destined for non-payment bank account defined by reg. | | `R21` | Invalid company ID number | The company ID information not valid (normally CIE entries) | | `R22` | Invalid individual ID number | Individual id used by receiver is incorrect (CIE entries) | | `R23` | Credit entry refused by receiver | Receiver returned entry because minimum or exact amount not remitted, bank account is subject to litigation, or payment represents an overpayment, originator is not known to receiver or receiver has not authorized this credit entry to this bank account | | `R24` | Duplicate entry | RDFI has received a duplicate entry | | `R25` | Addenda error | Improper formatting of the addenda record information | | `R26` | Mandatory field error | Improper information in one of the mandatory fields | | `R27` | Trace number error | Original entry trace number is not valid for return entry; or addenda trace numbers do not correspond with entry detail record | | `R28` | Transit routing number check digit error | Check digit for the transit routing number is incorrect | | `R29` | Corporate customer advises not authorized | RDFI has been notified by corporate receiver that debit entry of originator is not authorized | | `R30` | RDFI not participant in check truncation program | Financial institution not participating in automated check safekeeping application | | `R31` | Permissible return entry (CCD and CTX only) | RDFI has been notified by the ODFI that it agrees to accept a CCD or CTX return entry | | `R32` | RDFI non-settlement | RDFI is not able to settle the entry | | `R33` | Return of XCK entry | RDFI determines at its sole discretion to return an XCK entry; an XCK return entry may be initiated by midnight of the sixtieth day following the settlement date if the XCK entry | | `R34` | Limited participation RDFI | RDFI participation has been limited by a federal or state supervisor | | `R35` | Return of improper debit entry | ACH debit not permitted for use with the CIE standard entry class code (except for reversals) | | `R37` | Source Document Presented for Payment (Adjustment Entry) | The source document to which an ARC, BOC or POP entry relates has been presented for payment. RDFI must obtain a Written Statement and return the entry within 60 days following Settlement Date | | `R38` | Stop Payment on Source Document (Adjustment Entry) | A stop payment has been placed on the source document to which the ARC or BOC entry relates. RDFI must return no later than 60 days following Settlement Date. No Written Statement is required as the original stop payment form covers the return | | `R39` | Improper Source Document | The RDFI has determined the source document used for the ARC, BOC or POP entry to its Receiver's account is improper. | #### Used for ENR entries and are initiated by a Federal Government Agency | Code | Reason | Description | |----|-----|------| | `R40` | Return of ENR Entry by Federal Government Agency (ENR Only) | This return reason code may only be used to return ENR entries and is at the federal Government Agency's Sole discretion | | `R41` | Invalid Transaction Code (ENR only) | Either the Transaction Code included in Field 3 of the Addenda Record does not conform to the ACH Record Format Specifications contained in Appendix Three (ACH Record Format Specifications) or it is not appropriate with regard to an Automated Enrollment Entry. | | `R42` | Routing Number/Check Digit Error (ENR Only) | The Routing Number and the Check Digit included in Field 3 of the Addenda Record is either not a valid number or it does not conform to the Modulus 10 formula. | | `R43` | Invalid DFI Account Number (ENR Only) | The Receiver's account number included in Field 3 of the Addenda Record must include at least one alphameric character. | | `R44` | Invalid Individual ID Number/Identification Number (ENR only) | The Individual ID Number/Identification Number provided in Field 3 of the Addenda Record does not match a corresponding ID number in the Federal Government Agency's records. | | `R45` | Invalid Individual Name/Company Name (ENR only) | The name of the consumer or company provided in Field 3 of the Addenda Record either does not match a corresponding name in the Federal Government Agency's records or fails to include at least one alphameric character. | | `R46` | Invalid Representative Payee Indicator (ENR Only) | The Representative Payee Indicator Code included in Field 3 of the Addenda Record has been omitted or it is not consistent with the Federal Government Agency's records. | | `R47` | Duplicate Enrollment (ENR Only) | The Entry is a duplicate of an Automated Enrollment Entry previously initiated by a DFI. | #### Used for RCK entries only and are initiated by an RDFI | Code | Reason | Description | |----|-----|------| | `R50` | State Law Affecting RCK Acceptance | RDFI is located in a state that has not adopted Revised Article 4 of the UCC or the RDFI is located in a state that requires all canceled checks to be returned within the periodic statement | | `R51` | Item Related to RCK Entry is Ineligible or RCK Entry is Improper | The item to which the RCK entry relates was not eligible, Originator did not provide notice of the RCK policy, signature on the item was not genuine, the item has been altered or amount of the entry was not accurately obtained from the item. RDFI must obtain a Written Statement and return the entry within 60 days following Settlement Date | | `R52` | Stop Payment on Item (Adjustment Entry) | A stop payment has been placed on the item to which the RCK entry relates. RDFI must return no later than 60 days following Settlement Date. No Written Statement is required as the original stop payment form covers the return. | | `R53` | Item and RCK Entry Presented for Payment (Adjustment Entry) | Both the RCK entry and check have been presented for payment. RDFI must obtain a Written Statement and return the entry within 60 days following Settlement Date | #### Used by the ODFI for dishonored return entries | Code | Reason | Description | |----|-----|------| | `R61` | Misrouted Return | The financial institution preparing the Return Entry (the RDFI of the original Entry) has placed the incorrect Routing Number in the Receiving DFI Identification field. | | `R67` | Duplicate Return | The ODFI has received more than one Return for the same Entry. | | `R68` | Untimely Return | The Return Entry has not been sent within the time frame established by these Rules. | | `R69` | Field Error(s) | One or more of the field requirements are incorrect. | | `R70` | Permissible Return Entry Not Accepted/Return Not Requested by ODFI | The ODFI has received a Return Entry identified by the RDFI as being returned with the permission of, or at the request of, the ODFI, but the ODFI has not agreed to accept the Entry or has not requested the return of the Entry. | #### Used by the RDFI for contested dishonored return entries | Code | Reason | Description | |----|-----|------| | `R71` | Misrouted Dishonored Return | The financial institution preparing the dishonored Return Entry (the ODFI of the original Entry) has placed the incorrect Routing Number in the Receiving DFI Identification field. | | `R72` | Untimely Dishonored Return | The dishonored Return Entry has not been sent within the designated time frame. | | `R73` | Timely Original Return | The RDFI is certifying that the original Return Entry was sent within the time frame designated in these Rules. | | `R74` | Corrected Return | The RDFI is correcting a previous Return Entry that was dishonored using Return Reason Code R69 (Field Error(s)) because it contained incomplete or incorrect information. | | `R75` | Return Not a Duplicate | The Return Entry was not a duplicate of an Entry previously returned by the RDFI. | | `R76` | No Errors Found | The original Return Entry did not contain the errors indicated by the ODFI in the dishonored Return Entry. | #### Used by Gateways for the return of international payments | Code | Reason | Description | |----|-----|------| | `R80` | IAT Entry Coding Error | The IAT Entry is being returned due to one or more of the following conditions: Invalid DFI/Bank Branch Country Code, invalid DFI/Bank Identification Number Qualifier, invalid Foreign Exchange Indicator, invalid ISO Originating Currency Code, invalid ISO Destination Currency Code, invalid ISO Destination Country Code, invalid Transaction Type Code | | `R81` | Non-Participant in IAT Program | The IAT Entry is being returned because the Gateway does not have an agreement with either the ODFI or the Gateway's customer to transmit Outbound IAT Entries. | | `R82` | Invalid Foreign Receiving DFI Identification | The reference used to identify the Foreign Receiving DFI of an Outbound IAT Entry is invalid. | | `R83` | Foreign Receiving DFI Unable to Settle | The IAT Entry is being returned due to settlement problems in the foreign payment system. | | `R84` | Entry Not Processed by Gateway | For Outbound IAT Entries, the Entry has not been processed and is being returned at the Gateway's discretion because either (1) the processing of such Entry may expose the Gateway to excessive risk, or (2) the foreign payment system does not support the functions needed to process the transaction. | | `R85` | Incorrectly Coded Outbound International Payment | The RDFI/Gateway has identified the Entry as an Outbound international payment and is returning the Entry because it bears an SEC Code that lacks information required by the Gateway for OFAC compliance. | ### 4. Generate an ACH Return File When RDFI file transaction processing is complete, a return file can be generated. ```graphql mutation GenerateAchReturnFile{ ach { generateFile( input:{ configId: "b96d358e-50b8-4ae5-8b07-2e8f33f396c6" key: "nacha_file_return.ach" type: RDFI_RETURN } ) { key } } } ``` ### 5. Download ACH Return File ```graphql mutation DownloadAchReturn { files { createDownload(key: "nacha_file_return.ach") { downloadURL } } } ``` ## Journal Posting Lifecycle All ACH transactions are managed via workflows. Each individual ACH transaction is assigned a fixed `executionId` and state transitions on those tasks are ran by the ACH RDFI processor. Each state transition may post one or more transactions. > **Note:** > > Workflows allow you to easily observe all historical actions taken on a particular `executionId`. > > Learn more about workflows in ... ### Workflows The following workflows are utilized in the ACH RDFI processor. #### ACH RDFI Debit ```mermaid graph TD CREATE(CREATE
ACH_ENCUMBRANCE_DR
_) SETTLE(SETTLE
ACH_ENCUMBRANCE_REVERSAL_DR
ACH_SETTLE_DR
_) RETURN(RETURN
ACH_*_RETURN_CR
_) CREATE --> SETTLE CREATE --> RETURN SETTLE --> RETURN ``` #### ACH RDFI Credit ```mermaid graph TD CREATE(CREATE
ACH_ENCUMBRANCE_CR
_) SETTLE(SETTLE
ACH_ENCUMBRANCE_REVERSAL_CR
ACH_SETTLE_CR
_) RETURN(RETURN
ACH_*_RETURN_DR
_) CREATE --> SETTLE CREATE --> RETURN SETTLE --> RETURN ``` ### TranCodes ```json { "data": { "1": { "code": "SYS_ACH_ENCUMBRANCE_CANCEL_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CANCEL_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_CANCEL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "2": { "code": "SYS_ACH_ENCUMBRANCE_CANCEL_REVERSAL_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CANCEL_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_CANCEL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "3": { "code": "SYS_ACH_ENCUMBRANCE_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "4": { "code": "SYS_ACH_ENCUMBRANCE_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "5": { "code": "SYS_ACH_ENCUMBRANCE_RETURN_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_RETURN_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_RETURN_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "6": { "code": "SYS_ACH_ENCUMBRANCE_RETURN_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_RETURN_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_RETURN_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "7": { "code": "SYS_ACH_ENCUMBRANCE_REVERSAL_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "8": { "code": "SYS_ACH_ENCUMBRANCE_REVERSAL_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_CR'", "accountId": "uuid(params.accountId)", "layer": "ENCUMBRANCE", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_ENCUMBRANCE_REVERSAL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "ENCUMBRANCE", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "9": { "code": "SYS_ACH_FEE_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_FEE_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_FEE_CR'", "accountId": "uuid(params.feeAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "10": { "code": "SYS_ACH_FEE_REIMBURSE_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_FEE_REIMBURSE_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_FEE_REIMBURSE_DR'", "accountId": "uuid(params.feeAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.feeAmount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "11": { "code": "SYS_ACH_PENDING_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_PENDING_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "12": { "code": "SYS_ACH_PENDING_CANCEL_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_CANCEL_CR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_PENDING_CANCEL_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "13": { "code": "SYS_ACH_PENDING_CANCEL_REVERSAL_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_CANCEL_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_PENDING_CANCEL_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "14": { "code": "SYS_ACH_PENDING_REVERSAL_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_PENDING_REVERSAL_DR'", "accountId": "uuid(params.accountId)", "layer": "PENDING", "direction": "DEBIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_PENDING_REVERSAL_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "PENDING", "direction": "CREDIT", "units": "decimal.Neg(params.amount)", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "15": { "code": "SYS_ACH_SETTLE_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_SETTLE_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "16": { "code": "SYS_ACH_SETTLE_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_SETTLE_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "17": { "code": "SYS_ACH_SETTLE_RETURN_CR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_RETURN_CR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_SETTLE_RETURN_DR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] }, "18": { "code": "SYS_ACH_SETTLE_RETURN_DR", "params": [ { "name": "accountId", "type": "UUID", "description": "The account to place the hold on.", }, { "name": "settlementAccountId", "type": "UUID", "description": "The settlement account to use.", }, { "name": "feeAccountId", "type": "UUID", "description": "Optional fee account to use.", "default": "00000000-0000-0000-0000-000000000000" }, { "name": "feeAmount", "type": "STRING", "description": "Optional decimal amount of the fee.", "default": "0" }, { "name": "journalId", "type": "UUID", "description": "The journal to post transactions to.", }, { "name": "amount", "type": "STRING", "description": "The decimal amount.", }, { "name": "correlationId", "type": "STRING", "description": "Correlation identifier to group related transactions.", }, { "name": "effective", "type": "DATE", "description": "Effective date for the transaction.", }, { "name": "metadata", "type": "JSON", "description": "Metadata to attach to transaction.", "default": "{}" }, { "name": "settleOn", "type": "TIMESTAMP", "description": "settleOn timestamp for ACH settlements.", "default": "1970-01-01T00:00:00Z" } ], "transaction": { "effective": "params.effective", "journalId": "params.journalId", "correlationId": "params.correlationId", "externalId": "''", "description": "''", "metadata": "params.metadata" }, "entries": [ { "entryType": "'ACH_SETTLE_RETURN_DR'", "accountId": "uuid(params.accountId)", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", }, { "entryType": "'ACH_SETTLE_RETURN_CR'", "accountId": "uuid(params.settlementAccountId)", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "'USD'", "description": "''", "metadata": "{}", } ] } } } ``` ## Components of the ACH RDFI Processor The ACH RDFI Processor includes several core components: 1. **ACH File Management**: Supports upload and download of ACH files containing ACH transactions directed to the RDFI, initiating the process for distributing funds to beneficiary accounts. 2. **Transaction Posting**: Processes received transactions, crediting or debiting the relevant accounts as per the transaction instructions. 3. **Return Handling**: Manages any returns of transactions that cannot be completed, automatically generating return entries and files. 4. **ACH Reversals**: Processes ACH reversals submitted by an ODFI, ensuring that erroneous transactions are reversed in compliance with ACH standards and updating the ledgers accordingly. 5. **Status Monitoring**: Provides endpoints to track the status of received transactions, enabling RDFIs to maintain visibility into processing stages and outcomes. 6. **Error Reporting**: Offers detailed error reporting for transactions that encounter issues, allowing for efficient troubleshooting and resolution. 7. **Ledger Integration**: Ensures that all processed, returned, and reversed transactions are accurately reflected in the Twisp ledger, maintaining up-to-date financial records. ## API Operations The ACH RDFI API supports a suite of GraphQL operations for managing incoming ACH transactions: - **ACH Configuration Operations** - [`Query.ach.config()`](#): Get configuration for ACH protocol processing. - [`Mutation.ach.createConfig()`](#): Create an ACH protocol config. - [`Mutation.ach.updateConfig()`](#): Update an ACH protocol config. - **ACH File Operations** - [`Query.ach.file()`](#): Monitor the status of an processed ACH file, including transactions and return file generation. - [`Mutation.ach.generateFile()`](#): Generate an ACH file. - [`Mutation.ach.processFile()`](#): Process an uploaded ACH file. - [`Mutation.files.createDownload()`](#): Download ACH files containing ACH transactions for submission to the network. - [`Mutation.files.createUpload()`](#): Upload a new ACH file containing ACH transactions for processing. - **Transaction Operations** - [`Query.ach.transaction()`](#): Get details and status of an ACH transaction. - [`Query.ach.transactions()`](#): Query ACH transactions. - [`Mutation.ach.updateTransaction`](#): Update an ACH transaction. ## Further Reading For more information on ACH file structure and reception procedures, see the [ACH File Reception Guide](#). To integrate this API into your existing systems, refer to the [RDFI API Integration Tutorial](#). For more context on the ACH network and RDFI responsibilities, explore the [ACH Network Overview](#). --- # Your First ACH Payment Send your first ACH credit payment Source: https://www.twisp.com/docs/tutorials/ach/first-ach-payment In this tutorial, we will send our first ACH payment. We'll create a customer account, initiate a $50 credit payment, and generate an ACH file ready for transmission. ## What We'll Build By completing this tutorial, we will: - Create a customer account - Execute an ACH PUSH (credit) workflow - Generate an ACH file - Download the file for transmission - Verify the ledger entries This tutorial takes approximately 10 minutes to complete. ## Prerequisites Before we begin, you must have completed: - [Setting Up ACH Processing](/docs/tutorials/ach/setting-up-ach) - This tutorial requires the IDs from setup You'll need these IDs from the setup tutorial: ``` Journal ID: 8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a Settlement: 37f7e8a6-171f-411d-ad59-7b1f40f505ea Fee: 5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d Config ID: fe27128a-b331-4e0e-94f8-9a32443fee36 ``` ## Step 1: Create a Customer Account First, we'll create an account for our customer who will receive the ACH payment. ```graphql mutation CreateCustomer { createAccount( input: { accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" code: "customer.alice.checking" name: "Alice Smith - Checking" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId name code } } ``` **What Just Happened:** We created a checking account for Alice Smith with ID `c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c`. This account represents Alice's bank account in our system. **Expected Response:** ```json { "data": { "createAccount": { "accountId": "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c", "name": "Alice Smith - Checking", "code": "customer.alice.checking" } } } ``` ## Step 2: Create the ACH Payment Now we'll create an ACH credit payment for $50. This uses the ACH PUSH workflow, which is for sending money to customers. ```graphql mutation CreatePayment { workflow { execute( input: { executionId: "55af980c-c1bb-11f0-833c-069b540ea27c" code: "ACH_PUSH" task: "CREATE" params: { accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" configId: "fe27128a-b331-4e0e-94f8-9a32443fee36" routingNumber: "026009593", accountNumber: "12345678901234567", accountType: "checking", individualName: "Alice Smith", entryDescription: "XFER", amount: "50.00" effective: "2025-11-15" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" feeAmount: "0.25" correlationId: "tutorial-payment-001" metadata: "{\"customer\": \"Alice Smith\", \"purpose\": \"Tutorial Payment\"}" entryMetadata: "{\"description\": \"Tutorial Payment\"}" } } ) { executionId task output { state } } } } ``` **What Just Happened:** We executed the CREATE task of the ACH PUSH workflow. This: - Created an pending transaction (hold) on Alice's account for $50 - Charged a $0.25 processing fee - Assigned execution ID for tracking **Key Parameters:** - `code: "ACH_PUSH"` - This is the ACH PUSH workflow UUID - `task: "CREATE"` - First state: create and encumber funds - `amount: "50.00"` - $50 payment - `effective: "2025-11-15"` - The date the payment should settle (use tomorrow's date in production) - `correlationId: "tutorial-payment-001"` - Unique identifier for tracking **Expected Response:** ```json { "data": { "workflow": { "execute": { "executionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "task": "CREATE", "output": { "state": "CREATE" } } } } } ``` **Save the Execution ID:** Copy the `executionId` from the response. We'll need it in the next step: ``` executionId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` ## Step 3: Check the Balance Let's verify the pending balance was created by checking Alice's account balance. ```graphql query CheckBalance { balance( accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" currency: "USD" ) { settled { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } pending { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } available (layer:PENDING) { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } normalBalance { formatted(as:{locale:"en-US"}) } } } } ``` **Expected Response:** ```json { "data": { "balance": { "settled": { "drBalance": { "formatted": "$0.25" }, "crBalance": { "formatted": "$0.00" } }, "pending": { "drBalance": { "formatted": "$50.00" }, "crBalance": { "formatted": "$0.00" } }, "available": { "drBalance": { "formatted": "$50.25" }, "crBalance": { "formatted": "$0.00" }, "normalBalance": { "formatted": "-$50.25" } } } } } ``` **What This Means:** - **Settled Layer**: $0.25 debit (the fee was charged immediately) - **Pending Layer**: $50 debit (funds are reserved, not yet settled) - **Available Balance**: -$50.25 (fees + pending amount) The negative available balance indicates Alice would need $50.25 in her account for this transaction to clear. ## Step 4: Generate the ACH File Now we'll generate an ACH file containing our payment. ```graphql mutation GenerateFile { ach { generateFile( input: { configId: "fe27128a-b331-4e0e-94f8-9a32443fee36" fileKey: "tutorial-payment-20251114.ach" fileType: ODFI_PUSH_ONLY generateEmpty: false } ) { fileKey generated } } } ``` Now we'll submit the payment for inclusion in an ACH file. This moves the payment from pending to settled and invokes the following workflow automatically: ```graphql mutation SubmitPayment { workflow { execute( input: { executionId: "55af980c-c1bb-11f0-833c-069b540ea27c" code: "ACH_PUSH" task: "SUBMIT" params: { effective: "2025-11-15" } } ) { executionId task output { state } } } } ``` **What Just Happened:** We requested generation of an ACH file that includes: - Reversed the pending - Posted to the settled layer - All submitted PUSH (credit) transactions - NACHA-formatted, ready for transmission - Stored with key `tutorial-payment-20251114.ach` **Expected Response:** ```json { "data": { "ach": { "generateFile": { "fileKey": "tutorial-payment-20251114.ach", "generated": true } } } } ``` **If `generated: false`:** This means no transactions were ready for file generation. ## Step 5: Verify the Settled Balance Let's check the balance again to see the settled transaction. ```graphql query CheckBalance { balance( accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" currency: "USD" ) { settled { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } pending { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } available (layer:PENDING) { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } normalBalance { formatted(as:{locale:"en-US"}) } } } } ``` **Expected Response:** ```json { "data": { "balance": { "settled": { "drBalance": { "formatted": "$50.25" }, "crBalance": { "formatted": "$0.00" } }, "pending": { "drBalance": { "formatted": "$0.00" }, "crBalance": { "formatted": "$0.00" } }, "available": { "drBalance": { "formatted": "$50.25" }, "crBalance": { "formatted": "$0.00" }, "normalBalance": { "formatted": "-$50.25" } } } } } ``` **What Changed:** - **Settled Layer**: Now $50.25 debit ($50 payment + $0.25 fee) - **Pending Layer**: Cleared to $0 (funds moved to settled) - **Available Balance**: $50.25 debit (the finalized transaction amount) The payment is now finalized and in a file on the way to the Federal Reserve! ## Step 6: Download the ACH File Now we'll get a download URL for our generated file. ```graphql mutation GetDownloadURL { files { createDownload( key: "tutorial-payment-20251114.ach" ) { downloadURL downloadURLExpiration } } } ``` **Expected Response:** ```json { "data": { "files": { "createDownload": { "downloadURL": "https://s3.amazonaws.com/bucket/path?signature=...", "downloadURLExpiration": "2025-11-14T15:30:00Z" } } } } ``` **Download the File:** Use the URL to download the file (replace `` with actual URL from response): ```bash curl '' -o tutorial-payment-20251114.ach ``` ## Step 7: Inspect the ACH File Let's look at what's in the file we generated. Open `tutorial-payment-20251114.ach` in a text editor. You'll see a NACHA-formatted file with lines like: ``` 101 021000021 1234567892511150055A094101Test Bank Your Company 5220Your Company 1234567890PPDXFER 251114251117 1123456780000001 622026009593123456789012345670000005000 Alice Smith 0123456780000001 822000000100026009590000000000000000000050001234567890 123456780000001 9000001000001000000010002600959000000000000000000005000 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 ``` **What Each Line Means:** - **Line 1 (101...)**: File Header - Contains ODFI info - **Line 2 (5225...)**: Batch Header - Contains company info - **Line 3 (622...)**: Entry Detail - The actual $50 payment to Alice - **Line 4 (822...)**: Batch Control - Validates batch totals - **Line 5 (9000...)**: File Control - Validates file totals The file is ready to transmit to your ODFI! ## Step 9: View the Workflow Execution Let's review the complete workflow execution to see all the ledger entries. ```graphql query ViewExecution { workflow { execution( executionId: "55af980c-c1bb-11f0-833c-069b540ea27c" ) { executionId workflowId task params activities { action entityType entity { ... on Transaction { transactionId effective entries(first:10) { nodes { entryId accountId layer direction units } } } } } } } } ``` **What You'll See:** This shows all ledger transactions created by the workflow: - CREATE task transactions (pending + fee) - SUBMIT task transactions (reverse pending + settle) Each activity shows the double-entry accounting entries on customer and settlement accounts. ## What We Accomplished Let's review what we did: 1. ✅ Created a customer account 2. ✅ Created a $50 ACH credit payment 3. ✅ Verified the pending transaction was created 4. ✅ Generated an ACH file 5. ✅ Verified the payment settled 6. ✅ Downloaded the ACH file 7. ✅ Inspected the NACHA file format 8. ✅ Reviewed the workflow execution ## Key Concepts We Learned **Workflow States:** - CREATE: Pending funds, charge fees - SUBMIT: Settle funds, mark for file generation **Balance Layers:** - Pending: Temporary holds - Settled: Finalized transactions - Available: Calculated from settled - pending **File Generation:** - Collects all submitted transactions - Generates NACHA-formatted files - Ready for ODFI transmission ## What Happens Next? In production, you would: 1. Transmit the generated file to your ODFI via SFTP 2. Wait for confirmation (usually 1-2 business days) 3. Process any returns received from the RDFI For this tutorial, you now have: - A working ACH payment workflow - A valid NACHA file - Understanding of the complete process ## Try It Again Now that you understand the process, try creating another payment: 1. Create a new customer account (use a different UUID) 2. Execute CREATE with a different amount 3. Submit the payment 4. Generate a new file Each time you generate a file, all submitted payments will be included. ## Troubleshooting **Problem: "Execution not found"** Make sure you're using the correct `executionId` from the CREATE response in the SUBMIT mutation. **Problem: "File generated: false"** This means no transactions were ready. Check: - Did you run the CREATE task? - Is the effective date in the future or today? - Are there any errors in the workflow execution? **Problem: "Cannot download file"** The download URL expires after a short time. If it's expired: - Run the `createDownload` mutation again to get a new URL **Problem: "Balance doesn't match"** After CREATE: - Pending should show $50 DR - Settled should show $0.25 DR (fee) After SUBMIT: - Pending should be $0 - Settled should show $50.25 DR ## Next Steps Now that you've sent your first ACH payment, explore more: **Processing Guides:** - [Processing ACH Payments](/docs/guides/processing-ach-payments) - Production payment workflows - [Handling ACH Returns](/docs/guides/handling-ach-returns) - Managing payment returns - [Reconciling ACH Files](/docs/guides/reconciling-ach-files) - File validation and reconciliation **Reference Documentation:** - [ODFI Reference](/docs/reference/ach/odfi) - Complete ODFI API documentation - [File Operations](/docs/reference/ach/file-operations) - File upload and download operations - [Configuration](/docs/reference/ach/configuration) - ACH configuration reference ## Summary Congratulations! You've successfully: - Created your first ACH payment - Executed a complete workflow from creation to settlement - Generated a NACHA-formatted ACH file - Understood the balance layer transitions - Downloaded a production-ready ACH file You now have the foundation to build production ACH payment systems on Twisp! --- # Setting Up ACH Processing Learn how to set up ACH processing from scratch Source: https://www.twisp.com/docs/tutorials/ach/setting-up-ach In this tutorial, we will set up ACH processing on the Twisp platform. By the end, you will have a fully configured ACH processor ready to send and receive ACH transactions. ## What We'll Build By completing this tutorial, we will: - Create all required accounts for ACH processing - Set up a journal for ACH transactions - Configure a webhook endpoint for transaction decisioning - Create an ACH configuration - Verify everything works correctly This tutorial takes approximately 15 minutes to complete. ## Prerequisites Before we begin, you need: - A Twisp account with API access - Your GraphQL API endpoint URL - API credentials (authentication token) You do NOT need: - An ODFI relationship (that comes later for production) - Understanding of double-entry accounting - Prior ACH experience ## Step 1: Create the Journal We will start by creating a journal where all ACH transactions will be posted. ```graphql mutation CreateJournal { createJournal( input: { journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" name: "ACH Processing Journal" status: ACTIVE } ) { journalId name status } } ``` **What Just Happened:** We created a journal with ID `8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a`. This journal will track all ACH-related accounting entries. **Expected Response:** ```json { "data": { "createJournal": { "journalId": "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a", "name": "ACH Processing Journal", "status": "ACTIVE" } } } ``` ## Step 2: Create Required Accounts Next, we will create four accounts required for ACH processing. We'll create them all at once. ```graphql mutation CreateACHAccounts { # Settlement account - where funds transit settlement: createAccount( input: { accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" code: "settlement.ach" name: "ACH Settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId name } # Suspense account - for transactions to unknown accounts suspense: createAccount( input: { accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" code: "suspense.ach" name: "ACH Suspense" config: { enableConcurrentPosting: true } } ) { accountId name } # Exception account - for failed transactions exception: createAccount( input: { accountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" code: "exception.ach" name: "ACH Exception" config: { enableConcurrentPosting: true } } ) { accountId name } # Fee account - for ACH processing fees fee: createAccount( input: { accountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" code: "fee.ach" name: "ACH Fee Income" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId name } } ``` **What Just Happened:** We created four accounts: 1. **Settlement Account** (`37f7e8a6-171f-411d-ad59-7b1f40f505ea`) - All ACH funds flow through this account during processing 2. **Suspense Account** (`3171b0c2-6e9f-41aa-a5a6-ee927deb27cf`) - Holds transactions when the destination account can't be found 3. **Exception Account** (`4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c`) - Receives transactions that fail processing rules 4. **Fee Account** (`5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d`) - Collects ACH processing fees All accounts have `enableConcurrentPosting: true` to support high transaction volumes. **Expected Response:** ```json { "data": { "settlement": { "accountId": "37f7e8a6-171f-411d-ad59-7b1f40f505ea", "name": "ACH Settlement" }, "suspense": { "accountId": "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf", "name": "ACH Suspense" }, "exception": { "accountId": "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c", "name": "ACH Exception" }, "fee": { "accountId": "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d", "name": "ACH Fee Income" } } } ``` ## Step 3: Create the Webhook Endpoint Now we will create a webhook endpoint. This endpoint will receive requests for transaction decisioning when ACH files are processed. For this tutorial, we'll use a test endpoint URL. In production, you'll replace this with your actual webhook server. ```graphql mutation CreateWebhook { events { createEndpoint( input: { endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" status: ENABLED endpointType: ACH_PROCESSOR url: "https://webhook.site/unique-url-here" subscription: [] description: "ACH decisioning webhook" } ) { endpointId url status } } } ``` **What Just Happened:** We created a webhook endpoint with ID `b84512f1-a67e-4dc2-94dd-66c48b4d13fb`. When ACH files are processed, Twisp will send POST requests to the URL we specified. **For This Tutorial:** Use `https://webhook.site` to create a free test webhook URL: 1. Go to https://webhook.site 2. Copy the unique URL shown 3. Use that URL in the mutation above **Expected Response:** ```json { "data": { "events": { "createEndpoint": { "endpointId": "b84512f1-a67e-4dc2-94dd-66c48b4d13fb", "url": "https://webhook.site/your-unique-url", "status": "ENABLED" } } } } ``` ## Step 4: Create the ACH Configuration Now we will tie everything together by creating an ACH configuration. This tells the ACH processor which accounts to use and where to send webhooks. ```graphql mutation CreateACHConfig { ach { createConfiguration( input: { configId: "fe27128a-b331-4e0e-94f8-9a32443fee36" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" exceptionAccountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" odfiHeaderConfiguration: { immediateDestination: "021000021" immediateDestinationName: "Test Bank" immediateOrigin: "1234567890" immediateOriginName: "Your Company" } timeZone: "America/New_York" } ) { configId version timeZone } } } ``` **What Just Happened:** We created an ACH configuration that connects: - The journal we created - All four accounts - The webhook endpoint - ODFI header information for generating ACH files The `odfiHeaderConfiguration` contains placeholder values. When you're ready for production, you'll replace these with real values from your ODFI (bank). **Expected Response:** ```json { "data": { "ach": { "createConfiguration": { "configId": "fe27128a-b331-4e0e-94f8-9a32443fee36", "version": 1, "timeZone": "America/New_York" } } } } ``` ## Step 5: Verify the Configuration Let's verify everything was created correctly by querying our configuration. ```graphql query VerifySetup { ach { configuration( id: "fe27128a-b331-4e0e-94f8-9a32443fee36" ) { configId journalId settlementAccountId suspenseAccountId exceptionAccountId feeAccountId endpointId odfiHeaderConfiguration { immediateDestination immediateOrigin } timeZone version } } } ``` **Expected Response:** ```json { "data": { "ach": { "configuration": { "configId": "fe27128a-b331-4e0e-94f8-9a32443fee36", "journalId": "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a", "settlementAccountId": "37f7e8a6-171f-411d-ad59-7b1f40f505ea", "suspenseAccountId": "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf", "exceptionAccountId": "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c", "feeAccountId": "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d", "endpointId": "b84512f1-a67e-4dc2-94dd-66c48b4d13fb", "odfiHeaderConfiguration": { "immediateDestination": "021000021", "immediateOrigin": "1234567890" }, "timeZone": "America/New_York", "version": 1 } } } } ``` **What to Check:** - All IDs match what we created - Version is 1 (this is the first version) - Timezone is correct for your location If everything matches, congratulations! Your ACH processor is configured. ## What We Accomplished Let's review what we built: 1. ✅ Created a journal for ACH transactions 2. ✅ Created four required accounts (settlement, suspense, exception, fee) 3. ✅ Set up a webhook endpoint 4. ✅ Created an ACH configuration connecting everything 5. ✅ Verified the configuration is correct ## Save These IDs You'll need these IDs going forward. Save them somewhere safe: ``` Journal ID: 8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a Settlement: 37f7e8a6-171f-411d-ad59-7b1f40f505ea Suspense: 3171b0c2-6e9f-41aa-a5a6-ee927deb27cf Exception: 4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c Fee: 5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d Webhook: b84512f1-a67e-4dc2-94dd-66c48b4d13fb Config ID: fe27128a-b331-4e0e-94f8-9a32443fee36 ``` ## Next Steps Now that your ACH processor is set up, you're ready to send your first ACH payment! **Continue to the next tutorial:** - [First ACH Payment](/docs/tutorials/ach/first-ach-payment) - Send a $50 test payment and generate an ACH file **Production Checklist:** Before using ACH in production, you'll need to: - [ ] Establish a relationship with an ODFI (bank) - [ ] Get real ODFI header values from your bank - [ ] Update the `odfiHeaderConfiguration` with real values - [ ] Set up SFTP/FTPS for file transmission - [ ] Implement a production webhook server - [ ] Test with your ODFI's validation process ## Troubleshooting **Problem: "Account already exists" error** If you see this error, it means you already have an account with that ID. This is fine! You can either: - Use the existing accounts and skip account creation - Choose different UUIDs for your accounts **Problem: "Webhook endpoint creation failed"** Common causes: - Invalid URL format - make sure it starts with `https://` - Network issue - try again in a moment **Problem: "Configuration creation failed"** Check that: - All referenced IDs (journal, accounts, endpoint) exist - You haven't already created a configuration with this ID - All UUIDs are properly formatted ## Summary You've successfully set up ACH processing! We created: - A journal for transaction tracking - Four accounts for different processing scenarios - A webhook endpoint for transaction decisioning - An ACH configuration tying everything together You're now ready to process ACH transactions. The next tutorial will show you how to send your first payment. --- # Core Admin: Managing Tenants, Users, and Groups In this tutorial, we'll cover the administrative tasks involved in a Twisp ledger: managing tenants, users, and groups. Source: https://www.twisp.com/docs/tutorials/admin-management ## Introduction to Tenants, Users, and Groups In any organization, especially in the context of accounting and financial systems, managing access control and permissions is crucial to ensure data security and prevent unauthorized access. Twisp provides a robust system to manage access control through [Tenants](/docs/reference/graphql/types/object#tenant), [Users](/docs/reference/graphql/types/object#user), and [Groups](/docs/reference/graphql/types/object#group). ### Tenants A [Tenant](/docs/reference/graphql/types/object#tenant) in Twisp represents an environment within an organization, typically associated with a specific application, service, or set of resources. Tenants contain isolated ledgers, each deployed to a specific region. They play a vital role in isolating data and configurations between different environments. Each tenant is uniquely identified by an `accountId`, which in combination with an AWS region, is used to calculate the database tenant for data isolation purposes. By isolating the ledgers and associated resources, Tenants ensure that each environment remains independent, preventing accidental or unauthorized access to sensitive data from other environments. ### Users [Users](/docs/reference/graphql/types/object#user) are human members within an organization who interact with the Twisp accounting core. Each user is uniquely identified by their email address. Users can belong to multiple [Groups](/docs/reference/graphql/types/object#group), which define their permissions within the organization based on the associated policies of each group. The effective permissions of a user are determined by the combined set of policies from all their groups. ### Groups [Groups](/docs/reference/graphql/types/object#group) are a logical grouping of users within an organization. They are used to manage access control and permissions for users. Each Group can have one or more associated policies that define the allowed actions for its member users. Users can belong to multiple Groups, and their permissions are determined by the combined set of policies from all their groups. ## Working with Users and Groups ### Managing Users To create a new User, you can use the `admin.createUser` mutation, providing the necessary input such as the user's unique ID (UUID), email address, and the group IDs the user should belong to. **Request** ```graphql mutation AdminCreateUser { admin { createUser( input: { id: "9cc8bd28-a36d-502e-89fd-7f1410c1b90a" groupIds: ["d57bd759-73d5-4452-a73e-12b590324e35"] email: "george@twisp.com" } ) { id email groupIds } } } ``` **Response** ```json { "data": { "admin": { "createUser": { "id": "9cc8bd28-a36d-502e-89fd-7f1410c1b90a", "email": "george@twisp.com", "groupIds": ["d57bd759-73d5-4452-a73e-12b590324e35"] } } } } ``` > **Note:** > > Newly created users will automatically receive an invitation email with a link to the Twisp console. To update an existing User's details, such as their email address or group memberships, you can use the `admin.updateUser` mutation with the corresponding input. ### Managing Groups To create a new Group, use the `admin.createGroup` mutation, providing the necessary input such as the group's unique ID (UUID), name, description, and policy. **Request** ```graphql mutation AdminCreateGroup { admin { createGroup( input: { id: "917e123a-b89d-4ab5-b11c-cdf6aac80b63" name: "empty-policy-1" description: "A group with an empty policy" policy: "[]" } ) { name description policy } } } ``` **Response** ```json { "data": { "admin": { "createGroup": { "name": "empty-policy-1", "description": "A group with an empty policy", "policy": "[]" } } } } ``` To update an existing Group's details, such as the name, description, or policy, use the `admin.updateGroup` mutation with the appropriate input. **Request** ```graphql mutation AdminUpdateGroup { admin { updateGroup( input: { name: "empty-policy-1" description: "An empty policy layer will default to the base policy." policy: "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"], \"assertions\": {\"always false\": \"1 == 0\"}}]" } ) { name description policy } } } ``` **Response** ```json { "data": { "admin": { "updateGroup": { "name": "empty-policy-1", "description": "An empty policy layer will default to the base policy.", "policy": "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"], \"assertions\": {\"always false\": \"1 == 0\"}}]" } } } } ``` ### Deleting Users and Groups To delete an existing user or group, use the `admin.deleteUser` or `admin.deleteGroup` mutation. You will need to provide the email address of the user you want to delete, or the name of the group. For example, this mutation deletes the user with the specified email address and returns the deleted user's email: **Request** ```graphql mutation AdminDeleteUser { admin { deleteUser(email: "george@twisp.com") { email } } } ``` **Response** ```json { "data": { "admin": { "deleteUser": { "email": "george@twisp.com" } } } } ``` ## Creating a Tenant A Tenant represents an environment within an organization, used for isolating data and configurations for specific applications or services. By creating a Tenant, you can ensure that your different environments have separate ledgers and resources, improving organization and security. ### Using the `admin.createTenant` mutation To create a new [Tenant](/docs/reference/graphql/types/object#tenant), you will need to use the `admin.createTenant` mutation, providing the necessary details such as `id`, `accountId`, `name`, and `description`. The `accountId` is especially important, as it is used in combination with an AWS region to isolate the tenant's data. Here's an example of a GraphQL mutation to create a new tenant: **Request** ```graphql mutation AdminCreateTenant { admin { createTenant( input: { id: "72a0097f-239e-48ec-a417-49c318332ed6" accountId: "sandbox" name: "Sandbox" description: "Sandbox tenant for testing" } ) { accountId name } } } ``` **Response** ```json { "data": { "admin": { "createTenant": { "accountId": "sandbox", "name": "Sandbox" } } } } ``` ## Assigning Permissions and Access Control Permissions are determined by the policies associated with each [Group](/docs/reference/graphql/types/object#group) a [User](/docs/reference/graphql/types/object#user) belongs to. The effective permissions for a user are calculated based on the combined set of policies from all their groups. For example, if a user belongs to two groups with different policies, their effective permissions will be the result of evaluating both policies together. To assign groups and permissions to users, you can use the `admin.updateUser` mutation by providing the user's ID and the desired group IDs. This mutation allows you to update the user's group membership, determining their effective permissions within the organization. Here's an example: **Request** ```graphql mutation AdminUpdateUser { admin { updateUser( input: { email: "george@twisp.com" groupIds: ["152f0c89-6cba-53c9-955e-16d7cbc1f35e"] } ) { email groupIds } } } ``` **Response** ```json { "data": { "admin": { "updateUser": { "email": "george@twisp.com", "groupIds": ["152f0c89-6cba-53c9-955e-16d7cbc1f35e"] } } } } ``` In this example, the user's group is changed because a new group ID is provided. ### Verifying Effective Permissions for Users To ensure that users have the correct permissions, it is essential to verify their effective permissions, which are determined by the combined set of policies from all their associated groups. To check a user's effective permissions, review the policies associated with each group the user belongs to, taking note of any `ALLOW` or `DENY` effects on actions and resources. The user must have at least one `ALLOW` policy for each desired action and resource but will be blocked by any `DENY` policy on the same action and resource. ### Monitoring and Adjusting Access as Needed It is crucial to regularly monitor and adjust user access to maintain a secure environment. You can use the `admin` queries to retrieve details about users, groups, and their permissions within the organization. For example, to get a list of users and their associated groups, use the following query: **Request** ```graphql query GetAdminUsers { admin { users(first: 5) { nodes { email organizationId groupIds } } } } ``` **Response** ```json { "data": { "admin": { "users": { "nodes": [ { "email": "test@twisp.com", "organizationId": "932343ab-33ee-410d-9663-7ddaec4d5bfa", "groupIds": ["4dcef8d5-186b-4db1-aa00-8d5b2eee85f8"] } ] } } } } ``` ## Conclusion In this tutorial, we have explored the concepts and functionalities of [Tenants](/docs/reference/graphql/types/object#tenant), [Users](/docs/reference/graphql/types/object#user), and [Groups](/docs/reference/graphql/types/object#group) within the Twisp Accounting Core. We have learned the importance of managing access control and permissions to maintain a secure and well-organized environment. By following the steps outlined in this tutorial, you should now have a solid understanding of how to manage Tenants, Users, and Groups effectively within Twisp. --- # Designing a Tran Code In this tutorial, we will cover some of the factors that go into designing a transaction code. Source: https://www.twisp.com/docs/tutorials/advanced/designing-tran-codes ## Preliminary Questions Tran codes should mirror a type of transaction that your system handles. They are meant to encapsulate and abstract accounting activity, and so the first step to designing a good tran code is **having a clear understanding of what that activity is**. Thus, before we get into designing a tran codes, it is important to first clarify: - What **type** of transaction is this? ACH transfer? Credit card purchase? Foreign exchange? Something else? - Which **accounts** are involved? - Where is the money coming from and where is it going? - Are there any additional accounts that need to be debited/credited? - How many **entries** should be written to the ledger? - Which entries go on the debit side and which on the credit side? - Do these entries reflect a **settled** amount, or are they still **pending** a final settlement? ## Naming and Documenting The **type** of transaction should be used to give the tran code a good name through its `code` field. The `code` field is the primary human-friendly identifier. It should provide a concise indication of what the tran code does and is used for. We recommend using codes that just give enough information to be easily identifiable without being over-wordy. For consistency, we recommend using UPPER_SNAKE_CASE formatting for the `code`. Prefer: - ✅ `ACH_CREDIT` - ✅ `CARD_HOLD_CANCEL` - ✅ `INTEREST_ADJUSTMENT` Avoid: - ❌ `ACH` - ❌ `CancelHold` - ❌ `adjusting_interest_for_personal_loan_accounts` > **Note:** > > Depending on the size and complexity of your tran code library, you may choose to implement more formalized patterns and structures for naming your tran codes. For example, some organizations might use abbreviated versions of operations (`HLD`, `STL`, `DEP`, `CLR`) to keep tran code names extra terse. Of course, only so much information can be communicated through a short string of characters. Because tran codes act as the API for your funds flow, they should also be well documented. The `description` field is where this documentation can live. Use it to add additional context about why the tran code exists, how it should be used, and whatever other information would benefit those interacting with your ledger. This field supports Markdown formatting. ## Defining the Transaction & Ledger Entries Posted transactions and the ledger entries written are defined within the `transaction` and `entries` fields, respectively. These are effectively templates used to generate the [Transaction](/docs/reference/graphql/types/object#transaction) and [Entries](/docs/reference/graphql/types/object#entry) records. Within the `transaction` field, we can define values that describe important aspects of the transaction like its `effective` date and which `journal` it should be written to. Other [Transaction](/docs/reference/graphql/types/object#transaction) fields like the `correlationId` and `metadata` can also be defined here, although they are optional. The `entries` field is a list of the templates for the [Entries](/docs/reference/graphql/types/object#entry) written to the ledger. Each ledger entry must define its `accountId`, amount (in `units` and `currency`), `direction` (DEBIT or CREDIT), `layer` (SETTLED, PENDING, or ENCUMBRANCE), and `entryType`. Optionally, a `description` may be written here as well. > **Note:** > > The `entryType` for an entry is similar to the `code` for a tran code: it is a short identifier for describing the type of activity that the entry represents. In many cases, the `entryType` is just an extension of the `code`. For example, an `ACH_CREDIT_FEE` tran code might write ledger entries with types `ACH_CREDIT_FEE_DR` for the debit-side and `ACH_CREDIT_FEE_CR` for the credit-side entry. How these entry definitions are written depends upon the transaction type and other information gathered as part of the pre-design process. **Request** ```graphql mutation ACHCreditTC( $achCreditId: UUID! $journalId: Expression! $achSettlementAcctId: Expression! $exampleUserAcctId: Expression! ) { achCredit: createTranCode( input: { tranCodeId: $achCreditId code: "ACH_CREDIT" description: "An ACH credit into an account." transaction: { journalId: $journalId, effective: "date('2000-01-01')" } entries: [ { accountId: $achSettlementAcctId units: "decimal('11.25')" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: $exampleUserAcctId units: "decimal('11.25')" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "achCredit": { "tranCodeId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b" } } } ``` **Variables** ```json { "achCreditId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b", "journalId": "uuid('8345d4a6-e100-4f70-9a31-d458acb3553e')", "achSettlementAcctId": "uuid('38897a35-0de5-4055-9a87-dc5256fc96b7')", "exampleUserAcctId": "uuid('fcb5a92f-cafb-4076-93dd-5df6d759a482')" } ``` ## Parameterizing Inputs In most cases, not all of the information needed to write transactions and entries is available at the time of designing a tran code, but instead needs to be passed in at runtime (i.e. when the transaction is posted). For example, most transactions are not for fixed amounts, and so these amounts need to be specified when posting the transaction. This is where the `params` field of a tran code comes in. With the `params`, we can define parameters of a transaction which can then be referenced inside of the values defined for the `transaction` and `entries`. Say we wanted to write entries where an `amount` (in decimal units) is supplied at posting time. To do this, we need to do two things: 1. Define an `amount` parameter inside of the `params` object. 2. Reference the `amount` value from `params` within the `units` field of our entries. A modification to the above tran code definition shows how this parameterization would work: **Request** ```graphql mutation ACHCreditTC( $achCreditId: UUID! $journalId: Expression! $achSettlementAcctId: Expression! ) { achCredit: createTranCode( input: { tranCodeId: $achCreditId code: "ACH_CREDIT" description: "An ACH credit into an account." params: [ { name: "account", type: UUID, description: "Deposit account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "effective" type: DATE description: "Effective date for ACH transaction." } { name: "currency" type: STRING description: "Currency code for entries. Defaults to 'USD'." default: "USD" } ] transaction: { journalId: $journalId, effective: "params.effective" } entries: [ { accountId: $achSettlementAcctId units: "params.amount" currency: "params.currency" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "params.currency" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "achCredit": { "tranCodeId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b" } } } ``` **Variables** ```json { "achCreditId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b", "journalId": "uuid('8345d4a6-e100-4f70-9a31-d458acb3553e')", "achSettlementAcctId": "uuid('38897a35-0de5-4055-9a87-dc5256fc96b7')" } ``` Note the change to `params.amount` inside of the `units` fields. Because these fields accept [CEL expressions](/docs/reference/cel), we can reference fields on the runtime `params` object to access the values passed in. You can see the use of `params` in action: [Tran Code Invocation](/docs/reference/ledger/tran-codes#tran-code-invocation). In this way, the tran code can accept any number of parameterized values at runtime an inject them into the [Transaction](/docs/reference/graphql/types/object#transaction) and [Entries](/docs/reference/graphql/types/object#entry) written. --- # Exporting Data with Warehouse Learn how to export your Twisp ledger data using the warehouse export API. Source: https://www.twisp.com/docs/tutorials/advanced/export-data Twisp’s **Warehouse Export** feature allows you to export ledger data into files you can download using the [Files API](/docs/reference/graphql/mutations#files-"files"). This feature is available to all tenants, even if you do not have a managed data warehouse configured for your environment. This tutorial will guide you through using the export API, explain key parameters, and share best practices for effective and efficient data exports. ## Why Use Warehouse Export? - **Incremental & Historical Exports:** Export only new/changed records, or retrieve all historical versions. - **Chunked, Compressed Exports:** Data is written in compressed files suitable for large-scale download and ingestion. - **Supports All Ledger Data:** Export accounts, balances, entries, transactions, and more, in bulk. ## Example To initiate an export for all balances for a specific date, use the following GraphQL mutation: ```graphql mutation Export { warehouse { export( input: { entity: Balance version: LATEST format: JSON compression: GZIP destination: { files: { keyPrefix: "exports/2025/07/02/balance/" } } fromTimestamp: "2025-07-01T00:00:00.000Z" toTimestamp: "2025-07-02T00:00:00.000Z" } ) { id } } } ``` This starts an export job and returns an `id` you can use to track its progress. To check the status and list files created, use: ```graphql query ExportStatus { warehouse { export(id: "YOUR_EXPORT_ID") { id status # (e.g., RUNNING, FINISHED, FAILED) error } } files { list(keyPrefix: "exports/2025/07/02/balance/") } } ``` When finished, the files (e.g., `balance/000.json.gz`, `balance/001.json.gz`, etc.) are available for download through the Files API. --- ## Key Parameters | Parameter | Description | |--------------------|--------------------------------------------------------------------------------------------------| | `entity` | The type of data to export (`Balance`, `Entry`, `Account`, etc.). | | `version` | `LATEST` exports only the most recent version per record; `HISTORY` exports all record versions. | | `format` | Format for the file (`JSON` is recommended for most usage). | | `compression` | Output compression (`GZIP` recommended for efficient transfer). | | `destination` | Where exported files are stored in Twisp Files (e.g., `{ files: { keyPrefix: "my/path" } }`). | | `fromTimestamp` | (Optional) Start export with records created/modified after this timestamp (inclusive). | | `toTimestamp` | (Optional) End export with records before this timestamp (exclusive). | > **Note:** > > **Tip:** Use `fromTimestamp` and `toTimestamp` for incremental exports—ideal for extracting only the new or modified data since your last export. This enables efficient change data capture (CDC) workflows for data lakes and reporting pipelines. ## Understanding LATEST vs HISTORY - **`version: LATEST`** Exports just the most current version of every record in the chosen entity. Use this for up-to-date snapshots of your ledger (e.g., current balances, current state of accounts). - **`version: HISTORY`** Exports all versions for every record. This is useful for: - Auditing - Reconstructing point-in-time states (e.g., balance history, account history) - Regulatory compliance > **Note:** > > To pull **point-in-time balances or entries** for reporting, use `version: HISTORY` and filter using `fromTimestamp` and `toTimestamp` to get just the versions active during a desired period. ## Output Schema Details - **File Schema:** The exported data files follow the [Warehouse entity schemas](/docs/reference/ledger/warehouse#views-and-schemas) (e.g., `balance`, `entry`), so you can use the same field definitions. - **Amounts:** For types like `balance` and `entry`, number fields (such as monetary amounts) will include a stand-alone `_units` column representing the value as a number for convenience. ## Downloading Exported Files Once the export job has completed: 1. Use the `files.list` API to enumerate all files at your chosen prefix. 2. Download each file with the `files.createDownload` mutation to receive a presigned URL. ## Additional Tips - **Large Exports:** For very large exports, the system will automatically shard data into multiple files (e.g., `balance/000.json.gz`, `balance/001.json.gz`, etc.). - **Performance:** Using compressed (`GZIP`) export is highly recommended for both speed and cost savings. --- # Multi-Journal Accounting In this tutorial, we will explore how to perform multiple-journal accounting using the Twisp Accounting Core. Source: https://www.twisp.com/docs/tutorials/advanced/multi-journal-accounting Our primary focus will be on setting up a multi-journal ledger and posting various transactions across these journals to demonstrate how balances are materialized in accounts and account sets across journals. We will walk through the following steps: 1. Setup multiple journals and an account set in each journal 2. Post transactions to each journal 3. Query ledger entries across both journals ## Step 1: Multi Journal Setup We'll create two journals: **"Journal 1"** as the primary journal and **"Journal 2"** as the secondary journal. Then, we'll create two account sets, one for each journal, both with a credit-normal balance type. Next, create two accounts "Account A" and "Account B" and associate them with both account sets. Finally, create a transfer transaction code with the necessary parameters and transaction entries. **Request** ```graphql mutation MultiJournalSetup { j1: createJournal( input: { journalId: "41ee64d6-dcde-46dc-bdc5-c9164517402a" name: "Journal 1" description: "Primary journal" } ) { journalId } j2: createJournal( input: { journalId: "2d9355ac-1844-4378-8fae-7ccb104e98c9" name: "Journal 2" description: "Secondary journal" } ) { journalId } acct_set_1: createAccountSet( input: { accountSetId: "861f2709-5e62-4a97-8364-b4d3b0a3b31f" journalId: "41ee64d6-dcde-46dc-bdc5-c9164517402a" name: "Accounts" description: "Group entries in all accounts for primary journal" normalBalanceType: CREDIT } ) { accountSetId normalBalanceType } acct_set_2: createAccountSet( input: { accountSetId: "c21f202f-d92d-48e5-aaf0-d48865a98051" journalId: "2d9355ac-1844-4378-8fae-7ccb104e98c9" name: "Accounts" description: "Group entries in all accounts for secondary journal" normalBalanceType: CREDIT } ) { accountSetId normalBalanceType } acct_a: createAccount( input: { accountId: "80c06296-7afb-4725-8cdd-57c2ce881af2" code: "ACCT_A" name: "Account A" description: "Account A" normalBalanceType: CREDIT status: ACTIVE accountSetIds: [ "861f2709-5e62-4a97-8364-b4d3b0a3b31f" "c21f202f-d92d-48e5-aaf0-d48865a98051" ] } ) { accountId sets(first: 4) { nodes { accountSetId normalBalanceType } } } acct_b: createAccount( input: { accountId: "6d808949-437f-4f23-a345-b09225104808" code: "ACCT_B" name: "Account B" description: "Account B" normalBalanceType: CREDIT status: ACTIVE accountSetIds: [ "861f2709-5e62-4a97-8364-b4d3b0a3b31f" "c21f202f-d92d-48e5-aaf0-d48865a98051" ] } ) { accountId sets(first: 4) { nodes { accountSetId normalBalanceType } } } xfr: createTranCode( input: { tranCodeId: "8f50f1be-a6c5-4f42-a623-57db9ff9f543" code: "XFR" description: "Transfer money." params: [ { name: "effectiveDate", type: DATE } { name: "amount", type: DECIMAL } { name: "fromAccount" type: UUID description: "Account to send funds from." } { name: "toAccount" type: UUID description: "Recipient account. Required." } { name: "journal", type: UUID, description: "Journal ID. Required." } ] transaction: { journalId: "params.journal" effective: "params.effectiveDate" } entries: [ { accountId: "params.fromAccount" units: "params.amount" currency: "'USD'" entryType: "'XFR_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.toAccount" units: "params.amount" currency: "'USD'" entryType: "'XFR_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "j1": { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a" }, "j2": { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9" }, "acct_set_1": { "accountSetId": "861f2709-5e62-4a97-8364-b4d3b0a3b31f", "normalBalanceType": "CREDIT" }, "acct_set_2": { "accountSetId": "c21f202f-d92d-48e5-aaf0-d48865a98051", "normalBalanceType": "CREDIT" }, "acct_a": { "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "sets": { "nodes": [ { "accountSetId": "c21f202f-d92d-48e5-aaf0-d48865a98051", "normalBalanceType": "CREDIT" }, { "accountSetId": "861f2709-5e62-4a97-8364-b4d3b0a3b31f", "normalBalanceType": "CREDIT" } ] } }, "acct_b": { "accountId": "6d808949-437f-4f23-a345-b09225104808", "sets": { "nodes": [ { "accountSetId": "c21f202f-d92d-48e5-aaf0-d48865a98051", "normalBalanceType": "CREDIT" }, { "accountSetId": "861f2709-5e62-4a97-8364-b4d3b0a3b31f", "normalBalanceType": "CREDIT" } ] } }, "xfr": { "tranCodeId": "8f50f1be-a6c5-4f42-a623-57db9ff9f543" } } } ``` After completing this setup, our chart of accounts will look like this: | Name | Description | Normal Balance Type | Record Type | |-----------|---------------------------------------------------------|---------------------|-------------| | Account A | Account A | CREDIT | Account | | Account B | Account B | CREDIT | Account | | Accounts | Group entries in all accounts for the primary journal | CREDIT | AccountSet | | Accounts | Group entries in all accounts for the secondary journal | CREDIT | AccountSet | We'll also have a simple `XFR` tran code for moving money between accounts. ## Step 2: Post Transactions Next, let's post two transactions using the `XFR` transaction code created above to write some entries to each journal. In the first transaction, we'll move $11.33 from Account A to Account B in _Journal 1_. In the second transaction, we'll transfer $22.44 from Account B to Account A in _Journal 2_. **Request** ```graphql mutation PostTransactions { xfr_j1: postTransaction( input: { transactionId: "a3501651-3550-40a3-a5e4-a07d983348f8" tranCode: "XFR" params: { fromAccount: "80c06296-7afb-4725-8cdd-57c2ce881af2" toAccount: "6d808949-437f-4f23-a345-b09225104808" amount: "11.33" journal: "41ee64d6-dcde-46dc-bdc5-c9164517402a" effectiveDate: "2022-09-10" } } ) { transactionId entries(first: 10) { nodes { journalId accountId direction units currency } } } xfr_j2: postTransaction( input: { transactionId: "87e0c5f2-fc1e-4450-9129-aba1c00533c3" tranCode: "XFR" params: { fromAccount: "6d808949-437f-4f23-a345-b09225104808" toAccount: "80c06296-7afb-4725-8cdd-57c2ce881af2" amount: "22.44" journal: "2d9355ac-1844-4378-8fae-7ccb104e98c9" effectiveDate: "2022-09-11" } } ) { transactionId entries(first: 10) { nodes { journalId accountId direction units currency } } } } ``` **Response** ```json { "data": { "xfr_j1": { "transactionId": "a3501651-3550-40a3-a5e4-a07d983348f8", "entries": { "nodes": [ { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "direction": "DEBIT", "units": "11.33", "currency": "USD" }, { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "6d808949-437f-4f23-a345-b09225104808", "direction": "CREDIT", "units": "11.33", "currency": "USD" } ] } }, "xfr_j2": { "transactionId": "87e0c5f2-fc1e-4450-9129-aba1c00533c3", "entries": { "nodes": [ { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "6d808949-437f-4f23-a345-b09225104808", "direction": "DEBIT", "units": "22.44", "currency": "USD" }, { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "direction": "CREDIT", "units": "22.44", "currency": "USD" } ] } } } } ``` ## Step 3: Account Set Entries Finally, let's investigate the entries written in each journal by the transactions posted. We'll retrieve account sets 1 and 2 using their respective IDs. For each account set, we'll get the member accounts and entries. Also, we'll get the journals' balance (in USD). **Request** ```graphql query AccountSetEntries { acct_set_1: accountSet(id: "861f2709-5e62-4a97-8364-b4d3b0a3b31f") { members(first: 10) { nodes { ... on Account { accountId } } } entries(first: 20) { nodes { journalId accountId entryType layer direction units currency } } balance(currency: "USD") { journalId settled { normalBalance { units } drBalance { units } crBalance { units } } } } acct_set_2: accountSet(id: "c21f202f-d92d-48e5-aaf0-d48865a98051") { journalId members(first: 10) { nodes { ... on Account { accountId } } } entries(first: 20) { nodes { journalId accountId entryType layer direction units currency } } balance(currency: "USD") { journalId settled { normalBalance { units } drBalance { units } crBalance { units } } } } } ``` **Response** ```json { "data": { "acct_set_1": { "members": { "nodes": [ { "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2" }, { "accountId": "6d808949-437f-4f23-a345-b09225104808" } ] }, "entries": { "nodes": [ { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "6d808949-437f-4f23-a345-b09225104808", "entryType": "XFR_CR", "layer": "SETTLED", "direction": "CREDIT", "units": "11.33", "currency": "USD" }, { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "entryType": "XFR_DR", "layer": "SETTLED", "direction": "DEBIT", "units": "11.33", "currency": "USD" } ] }, "balance": { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "settled": { "normalBalance": { "units": "0.00" }, "drBalance": { "units": "11.33" }, "crBalance": { "units": "11.33" } } } }, "acct_set_2": { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "members": { "nodes": [ { "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2" }, { "accountId": "6d808949-437f-4f23-a345-b09225104808" } ] }, "entries": { "nodes": [ { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "entryType": "XFR_CR", "layer": "SETTLED", "direction": "CREDIT", "units": "22.44", "currency": "USD" }, { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "6d808949-437f-4f23-a345-b09225104808", "entryType": "XFR_DR", "layer": "SETTLED", "direction": "DEBIT", "units": "22.44", "currency": "USD" } ] }, "balance": { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "settled": { "normalBalance": { "units": "0.00" }, "drBalance": { "units": "22.44" }, "crBalance": { "units": "22.44" } } } } } } ``` For easier reading, here are the entries listed out: | Type | Journal | Account | Direction | Amount | |----------|-----------|-----------|-----------|--------| | `XFR_CR` | Journal 1 | Account B | `CREDIT` | $11.33 | | `XFR_DR` | Journal 1 | Account A | `DEBIT` | $11.33 | | `XFR_CR` | Journal 2 | Account A | `CREDIT` | $22.44 | | `XFR_DR` | Journal 2 | Account B | `DEBIT` | $22.44 | ## Summary We began by establishing two separate journals and creating account sets and accounts for each. We then created a transfer transaction code that allowed us to post transactions across these journals. By posting two sample transactions, we illustrated how balances are materialized in accounts and account sets across different journals. This tutorial provides a simplified into managing complex financial scenarios where multiple journals are required, such as consolidating the financial activity of multiple subsidiaries in a parent company or tracking transactions in various currencies. --- # Void and Post Transactions Learn how to replace transactions with Twisp's voidTransaction and postTransaction mutations. Source: https://www.twisp.com/docs/tutorials/advanced/void-and-post 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** ```mermaid flowchart TD subgraph Authorization C["Customer swipes card at merchant"] POS["Merchant POS"] NET["Network"] ISS["Issuer Bank Processor"] LEDGERP["Ledger entry: PENDING"] C --> POS POS -- "Auth request" --> NET NET -- "Auth request" --> ISS ISS -- "Check balances, Write PENDING" --> LEDGERP ISS -- "Auth response" --> NET NET -- "Auth response" --> POS end ``` **Step 2: Capture/Settle** ```mermaid flowchart TD subgraph Capture/Settlement POS2["Merchant POS (Capture)"] NET2["Network"] ISS2["Issuer Bank Processor"] LEDGERS["Ledger entry: SETTLED"] POS2 -- "Capture (settlement)" --> NET2 NET2 -- "Settle request" --> ISS2 ISS2 -- "Void PENDING, Write SETTLED" --> LEDGERS end ``` 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_POST` workflow 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.](/docs/tutorials/example-setup) - 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. ```graphql 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). ```graphql 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: ```graphql 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. ```graphql 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 `voidedBy` populated - a void transaction with `voidOf` pointing back to the authorization - the settled capture on the `SETTLED` layer ## 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: ```graphql 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:** > > 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: ```graphql 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. --- # Making API Requests with cURL In this tutorial, we will learn how to make requests against the Twisp GraphQL API using the curl command line tool. Source: https://www.twisp.com/docs/tutorials/api/making-api-requests-with-curl **Prerequisites** To complete this tutorial, you'll need: - The URL endpoint for the API - The `accountId` of your tenant - An authenticated JWT --- To make HTTPS requests to the Twisp GraphQL API using cURL, follow these steps: 1. Open a command-line interface (Terminal on macOS/Linux or Command Prompt on Windows). 2. Type in the following command: ```shell curl 'https://api..cloud.twisp.com/financial/v1/graphql' \ ``` This line specifies the URL to which the cURL request will be made. Replace `` with the AWS region for your ledger, e.g. `us-east-1`. 3. Add the required headers by typing the following lines: ```shell -H 'authorization: Bearer ' \ -H 'content-type: application/json, application/json' \ -H 'x-twisp-account-id: ' \ ``` Replace `` with your authenticated JWT and `` with your Twisp account ID for the tenant. The `-H` flag is used to specify custom headers. In this case, the headers being set are: - `authorization` for authentication purposes. - `content-type` to indicate the type of data being sent in the request body. - `x-twisp-account-id` for specifying the account ID to be used in the request. 4. Add the request payload by typing the following line: ```shell --data-raw '{"query":"query { accounts(index: { name: STATUS }, where: { status: { eq: "ACTIVE" } }, first: 5) { nodes { accountId code name } } }"}' ``` This line contains the `--data-raw` flag followed by a JSON string. This JSON string includes a GraphQL query to fetch accounts with the specified conditions (in this case, the first 5 active accounts). This is where you write your GraphQL operation. 5. After entering all the lines, the complete cURL request should look like: ```shell curl 'https://api..cloud.twisp.com/financial/v1/graphql' \ -H 'authorization: Bearer ' \ -H 'content-type: application/json, application/json' \ -H 'x-twisp-account-id: ' \ --data-raw '{"query":"query { accounts(index: { name: STATUS }, where: { status: { eq: "ACTIVE" } }, first: 5) { nodes { accountId code name } } }"}' ``` 6. Press Enter to execute the cURL request. If successful, you should receive a JSON response containing the requested data. --- # Querying Paginated Fields In this tutorial, we will learn to handle cursor-paginated fields in the GraphQL schema. Source: https://www.twisp.com/docs/tutorials/api/querying-paginated-fields ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). --- ## 1. Identify the connection field Any field that resolves to a `*Connection` type uses cursor-based pagination. This includes top-level queries like `entries` and `transactions`, as well as fields within object types like `Account.balances`. ## 2. Request edges and nodes To fetch data in pages, request the `edges` in the connection field. Each edge represents a link to a data node and may contain additional information about the relationship. Within each edge, request the `node` field to retrieve the actual data object. Here's a sample query for fetching the first 10 items of a field called `someConnection`: ```graphql query { someConnection(first: 10) { edges { node { id name } } } } ``` > **Note:** > > Connection types also contain a `nodes` field, which is just a way to more directly access the list of `node` objects in `edges`. It can be useful in cases where you don't need to query any other fields of the edge object. ## 3. Get the page info Alongside the edges, request the `pageInfo` object, which contains information about pagination. `pageInfo` includes the fields `hasNextPage`, `hasPreviousPage`, `startCursor`, and `endCursor`. Add the `pageInfo` field to your query: ```graphql query { someConnection(first: 10) { edges { node { id name } } pageInfo { hasNextPage endCursor } } } ``` ## 4. Paginate using cursors Cursors are opaque strings representing the position of an item in the list. Use the `after` argument in conjunction with the `first` argument to request the next set of _n_ items after the `endCursor`: ```graphql query { someConnection(first: 10, after: "") { edges { node { id name } } pageInfo { hasNextPage endCursor } } } ``` Replace `""` with the actual `endCursor` value from the previous `pageInfo`. ## 5. Iterate through pages Continue making requests using the updated `endCursor` until the `hasNextPage` field in `pageInfo` is `false`, indicating that there are no more pages to fetch. --- # Building Tran Codes In this tutorial, we will learn how to manage transaction codes using the GraphQL API. Source: https://www.twisp.com/docs/tutorials/building-tran-codes Transaction codes play a critical role in identifying and categorizing transactions in the ledger, and designing them is an important part of using Twisp. By following the steps outlined in this tutorial, you will be able to create and manage transaction codes in your own ledger. > **Task:** > > - Create a new tran code using the `createTranCode` mutation > - Update fields on an existing tran code using the `updateTranCode` mutation > - Get data about a tran code with the `tranCode` query > - Delete (lock) a tran code using the `deleteTranCode` mutation --- ## Prerequisites Before you start, you should have added accounts to your ledger. See the tutorial on [Setting Up Accounts](/docs/tutorials/setting-up-accounts). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Create a new tran code To create a new tran code, use the `createTranCode` mutation. The `input` argument specifies the full configuration of the tran code and how transactions posted with this code will be structured, including all entries written to the ledger. Many of the fields in the `TranCodeInput` object (and its nested objects) allow for CEL expressions to be used, which will be evaluated during the `postTransaction` call and the results will be used in the creation of a transaction posted with the new transaction code. Note that the `params` field allows for the specification of parameters that can be used in CEL expressions for the transaction and entries fields. These parameters can be used to make the transaction code more flexible and dynamic, allowing for the creation of more complex transactions. The `transaction` and `entries` fields, respectively, act as templates for the [Transaction](/docs/reference/graphql/types/object#transaction) and [Entries](/docs/reference/graphql/types/object#entry) that are created when this tran code is used in a `postTransaction` call. Here's an example of creating a new tran code for a `BOOK_TRANSFER`: **Request** ```graphql mutation CreateTranCode( $tcBookTransferId: UUID! $journalGLIdExp: Expression! ) { createTranCode( input: { tranCodeId: $tcBookTransferId code: "BOOK_TRANSFER" description: "Book transfer between two internal accounts." metadata: { category: "Internal" } params: [ { name: "crAccount", type: UUID, description: "Account to credit." } { name: "drAccount", type: UUID, description: "Account to debit." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "currency" type: STRING description: "Currency used for transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } ] vars: { amount2: "decimal('1.00')", amount3: "this.amount2" } transaction: { journalId: $journalGLIdExp effective: "params.effective" description: "'Book transfer for $' + string(params.amount)" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } } } ``` **Variables** ```json { "journalGLIdExp": "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')", "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` Once created, transactions can be posted using this transaction code by providing the required fields in a `TransactionInput` object. The inputs for a `postTransaction` using this tran code must include fields for the `crAccount`, `drAccount`, `amount`, `currency`, and `effective`. > **Note:** > > Designing tran codes is one of the most important parts of using Twisp, and it can take some time to familiarize yourself with the process. > > Read more about tran codes on the [Encoded Transactions](/docs/accounting-core/encoded-transactions) page. ## Query tran codes The `tranCode` query can be used to read back an existing tran code using its `tranCodeId`. Here is an example query: **Request** ```graphql query GetBookTransferTranCode { tranCode(id: "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855") { code description params { name type description } transaction { journalId effective correlationId description } entries { accountId layer direction units currency } status metadata } } ``` **Response** ```json { "data": { "tranCode": { "code": "BOOK_TRANSFER", "description": "Book transfer between two internal accounts.", "params": [ { "name": "crAccount", "type": "UUID", "description": "Account to credit." }, { "name": "drAccount", "type": "UUID", "description": "Account to debit." }, { "name": "amount", "type": "DECIMAL", "description": "Amount with decimal, e.g. `1.23`." }, { "name": "currency", "type": "STRING", "description": "Currency used for transaction." }, { "name": "effective", "type": "DATE", "description": "Effective date for transaction." } ], "transaction": { "journalId": "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')", "effective": "params.effective", "correlationId": "''", "description": "'Book transfer for $' + string(params.amount)" }, "entries": [ { "accountId": "params.drAccount", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "params.currency" }, { "accountId": "params.crAccount", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "params.currency" } ], "status": "ACTIVE", "metadata": { "category": "Internal" } } } } ``` This query will respond with the fields for the `BOOK_TRANSFER` tran code created above, identified by its UUID. The `params` field in the response includes information about the parameters required by this transaction code, such as the name, type, and description of each parameter. The `transaction` field in the response includes details about the transaction that is created when this transaction code is used. The `entries` field in the response includes information about the entries that are created when this transaction code is used. Finally, the response includes the status of the transaction code, which can be either `ACTIVE` or `LOCKED`, and any metadata associated with the transaction code. ## Modify an existing tran code To modify an existing tran code, use the `updateTranCode` mutation. Provide the tran code's `id` and the fields to update in the `TranCodeUpdateInput` input object. You can only modify a subset of fields for data integrity purposes. Here's an example of modifying the description of an existing tran code: **Request** ```graphql mutation UpdateTranCode($tcBookTransferId: UUID!) { updateTranCode( id: $tcBookTransferId input: { description: "Book transfer between two customer wallet accounts." vars: { hello: "string('world')" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" metadata: "{'first': 1}" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" metadata: "{'second':2}" } ] } ) { tranCodeId description entries { metadata } vars history(first: 2) { nodes { version description vars } } } } ``` **Response** ```json { "data": { "updateTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855", "description": "Book transfer between two customer wallet accounts.", "entries": [ { "metadata": "{'first': 1}" }, { "metadata": "{'second':2}" } ], "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2", "hello": "string('world')" }, "history": { "nodes": [ { "version": 2, "description": "Book transfer between two customer wallet accounts.", "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2", "hello": "string('world')" } }, { "version": 1, "description": "Book transfer between two internal accounts.", "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2" } } ] } } } } ``` **Variables** ```json { "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` This operation updates the description of a transaction code. The response includes the UUID and the updated description of the transaction code, as well as the history of the changes made to the transaction code. ## Lock a tran code Because Twisp is an immutable database, we cannot fully "delete" a tran code. Instead, Twisp marks the tran code's status as `LOCKED`, which prevents transactions from posting using this version of tran code. To delete (lock) an tran code, we can use the `deleteTranCode` mutation. We need to provide the `id` of the tran code we want to delete. Here's an example mutation: **Request** ```graphql mutation DeleteTranCode($tcBookTransferId: UUID!) { deleteTranCode(id: $tcBookTransferId) { tranCodeId status } } ``` **Response** ```json { "data": { "deleteTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855", "status": "LOCKED" } } } ``` **Variables** ```json { "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` Once the tran code is deleted, this mutation returns two fields: `tranCodeId` and `status`. `tranCodeId` is a UUID that uniquely identifies the deleted tran code, while `status` is the current status of the tran code (i.e. `LOCKED`). This mutation is useful if you need to remove an tran code that is no longer needed or was created in error. ## Conclusion This tutorial covered the basics of creating, modifying, and deleting transaction codes. Transaction codes play a critical role in identifying and categorizing transactions in the ledger, and designing them is an important part of using Twisp. By following the steps outlined in this tutorial, you should be able to create and manage transaction codes in your own ledger. --- # Implementing Custom Calculations Learn how to define, create, and use custom calculations in Twisp to track specialized balances (e.g., by effective date or metadata tags) and query them efficiently using the GraphQL API. Source: https://www.twisp.com/docs/tutorials/calculations Custom calculations allow you to define specialized ways to aggregate and query balances based on dimensions beyond the standard `accountId`, `journalId`, and `currency`. This enables tracking balances grouped by criteria like effective date, specific metadata tags, or other transaction/entry attributes relevant to your business logic. By the end of this tutorial, you will be able to design and create a custom calculation, and then query the specialized balances it generates. > **Task:** > > - Design a custom calculation with specific dimensions using CEL. > - Create the calculation using the `createCalculation` mutation. > - Query the calculated balances using the `balances` query with the `CALCULATION` index. > - (Optional) Attach a `LOCAL` scope calculation to specific accounts or sets. --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Designing a Custom Calculation Before creating a calculation, you need to define how you want to group and filter balances. Let's design a calculation to track balances *per effective date* for each account. Key components of a calculation definition: * **`calculationId`:** A unique UUID you provide to identify this calculation. * **`code`:** A unique, human-readable string code for the calculation (e.g., `PER_EFFECTIVE_DATE`). * **`description`:** Explains the purpose of the calculation. * **`scope`:** * `GLOBAL`: The calculation applies to *all* accounts and sets within the journal(s). This is the default. * `LOCAL`: The calculation *only* applies to accounts or sets where it has been explicitly attached using the `attachCalculation` mutation. * **`dimensions`:** An array defining *how* the balance is indexed or grouped. Each dimension requires: * `alias`: A name for the dimension (e.g., `effectiveDate`). * `value`: A [CEL expression](https://twisp.com/docs/reference/cel) referencing `context.vars` (which contains the `entry`, `transaction`, `account`, etc. being processed) to get the dimension's value (e.g., `context.vars.transaction.effective` to group by the transaction's effective date). * **`condition` (Optional):** A CEL expression that must evaluate to `true` for an entry to be included in this calculation. If omitted, all entries matching the dimensions are included. Example: `context.vars.entry.amount.units() > decimal('0.0')` would only include entries with positive amounts. For our example (tracking balance per effective date): * **`calculationId`:** `"5867b5dd-fc69-416c-80f5-62e8a53610d5"` (Use your own unique UUID) * **`code`:** `PER_EFFECTIVE_DATE` * **`description`:** `Track balances per effective date in an account.` * **`scope`:** `GLOBAL` (applies everywhere by default) * **`dimensions`:** `alias: "effectiveDate"`, `value: "context.vars.transaction.effective"` * **`condition`:** None (include all entries) ## Create the Custom Calculation Use the `createCalculation` mutation with the parameters defined above. ```graphql mutation CreateCalculationPerEffectiveDate { createCalculation( input: { calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" # Use your own UUID code: "PER_EFFECTIVE_DATE" description: "Track balances per effective date in an account." scope: GLOBAL dimensions: [ { alias: "effectiveDate" value: "context.vars.transaction.effective" } ] # condition: "context.vars.entry.layer == 'SETTLED'" # Optional example condition } ) { calculationId code description scope dimensions { alias value } condition version created modified } } ``` ```json { "data": { "createCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "code": "PER_EFFECTIVE_DATE", "description": "Track balances per effective date in an account.", "scope": "GLOBAL", "dimensions": [ { "alias": "effectiveDate", "value": "context.vars.transaction.effective" } ], "condition": null, "version": 1, "created": "2023-10-27T12:00:00Z", "modified": "2023-10-27T12:00:00Z" } } } ``` This mutation creates the `PER_EFFECTIVE_DATE` calculation. The response confirms its structure. Twisp will now start computing balances grouped by `accountId`, `journalId`, `currency`, *and* `effectiveDate`. ## Query Using the Custom Calculation To retrieve balances computed by a custom calculation, use the standard [`balances`](/docs/reference/graphql/queries#balances) query, specifying `index: { name: CALCULATION }`. In the `where` clause, you **must** provide: * The [`accountId`](/docs/reference/graphql/types/input#balance-filter-input:account-id) (or `accountSetId`). * The [`calculationId`](/docs/reference/graphql/types/input#balance-filter-input:calculation-id) of your custom calculation. * A [`dimension`](/docs/reference/graphql/types/input#balance-filter-input:dimension) filter matching the structure defined in your calculation. Provide the specific dimension `alias`(es) and the `value`(s) you want to query (e.g., a specific date for the `effectiveDate` alias). ```graphql query GetCalculationBalanceForDate($accountId: UUID!, $calcId: UUID!, $journalId: UUID) { balances( index: { name: CALCULATION } where: { accountId: { eq: $accountId } journalId: { eq: $journalId } # Optional: Specify journal or uses default calculationId: { eq: $calcId } # ID of the custom calculation dimension: { # Alias must match the one defined in the calculation effectiveDate: "2023-09-21" # Specific dimension value to query } currency: { eq: "USD" } # Optional: Filter by currency if needed } first: 10 ) { nodes { accountId journalId currency calculationId # Confirms which calculation produced this balance dimensions # Shows the specific dimension values for this balance record settled { normalBalance { units } } # Query other balance layers (pending, encumbrance) or fields as needed } pageInfo { hasNextPage endCursor } } } ``` **Variables for the query:** ```json { "accountId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "calcId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` **Example Response:** ```json { "data": { "balances": { "nodes": [ { "accountId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "currency": "USD", "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "dimensions": { "effectiveDate": "2023-09-21" }, "settled": { "normalBalance": { "units": "5.25" } } } // ... other balances matching the criteria might appear if pagination allows ], "pageInfo": { "hasNextPage": false, "endCursor": "..." } } } } ``` This query retrieves the balance record specifically calculated for the account `"1fd1dd3e-..."`, using the `"PER_EFFECTIVE_DATE"` calculation, for the exact effective date `"2023-09-21"`. The `dimensions` field in the response confirms the `effectiveDate` value associated with this particular balance record. ## Attaching Calculations (`LOCAL` Scope Only) If you created a calculation with `scope: LOCAL`, it only computes balances for accounts or sets where it's explicitly attached. `GLOBAL` calculations apply automatically and do not need attaching. Use the [`attachCalculation`](/docs/reference/graphql/mutations#attach-calculation) mutation: * Provide the `calculationId` of the `LOCAL` calculation. * Provide the `accountId` of the target [Account](/docs/reference/graphql/types/object#account) or [AccountSet](/docs/reference/graphql/types/object#account-set). * Optionally provide the `journalId` if attaching to an `Account` and not using the default journal (ignored for `AccountSet`). ```graphql # Example: Attaching a LOCAL calculation to an account mutation AttachLocalCalcToAccount($calcId: UUID!, $accountId: UUID!, $journalId: UUID) { attachCalculation( input: { calculationId: $calcId # ID of the LOCAL calculation accountId: $accountId # ID of the Account or AccountSet to attach to journalId: $journalId # Optional: Specify journal for Account attachment } ) { # Response confirms attachment details calculationId accountId journalId calculation { code } account { name } } } ``` **Variables for the mutation:** ```json { "calcId": "YOUR_LOCAL_CALCULATION_ID", "accountId": "YOUR_TARGET_ACCOUNT_ID", "journalId": "YOUR_JOURNAL_ID" } ``` > **Note:** > > Creating a GLOBAL calculation or attaching a LOCAL calculation will initiate a backfill of that calculation using historical entries. It may take a few minutes for _new_ entries to show up in the balance while the backfill is ongoing. ## Conclusion In this tutorial, you learned how to design custom calculations by defining dimensions and optional conditions using CEL. You created a calculation using the `createCalculation` mutation and queried its specialized balances via the `balances` query with the `CALCULATION` index and dimension filters. Finally, you saw how to attach `LOCAL` scope calculations to specific accounts or sets using `attachCalculation`. Custom calculations offer powerful flexibility for tracking and querying balances based on criteria specific to your application's needs, enabling more granular financial insights and reporting. --- # Example Setup Use this example ledger configuration to start exploring Twisp and learning how to build your accounting system. Source: https://www.twisp.com/docs/tutorials/example-setup This setup is for an example budgeting app called "Budget Buddy". Think of it as a mashup of a budgeting tool like [Mint](https://mint.intuit.com/) or [YNAB](https://www.ynab.com/) with a social wallet app like [Venmo](https://venmo.com/). **The full setup GraphQL code can be found at the bottom of this page.** ## Account Structure Budget Buddy's chart of accounts includes account sets for each user, containing that user's wallet, budget accounts, and linked financial accounts like checking or credit cards. It also has offset and settlement account. ### User Accounts ```mermaid graph BT bert[/Bert\] ernie[/Ernie\] users[/Users\] bert & ernie --> users ``` ```mermaid graph BT bert[/Bert\] bert_budget[Bert's Budget] bert_cash[Bert's Cash Acct.] bert_checking[Bert's Checking Acct.] bert_wallet[Bert's Wallet] bert_budget & bert_cash & bert_checking & bert_wallet --> bert ``` ```mermaid graph BT ernie[/Ernie\] ernie_budget[Ernie's Budget] ernie_checking[Ernie's Checking Acct.] ernie_credit_card[Ernie's Credit Card Acct.] ernie_wallet[Ernie's Wallet] ernie_budget & ernie_checking & ernie_credit_card & ernie_wallet --> ernie ``` ### Settlement Accounts ```mermaid graph BT settlement[/Settlement\] settlement_card[Card Settlement] settlement_cash[Cash Settlement] settlement_checking[Checking Settlement] settlement_card & settlement_cash & settlement_checking --> settlement ``` ### Additional Sets Additional account sets are used to roll up wallet and budget accounts: ```mermaid graph BT budgets[/Budgets\] wallets[/Wallets\] bert_budget[Bert's Budget] bert_wallet[Bert's Wallet] ernie_budget[Ernie's Budget] ernie_wallet[Ernie's Wallet] budget_offset[Budget Offset] budget_offset & bert_budget & ernie_budget --> budgets bert_wallet & ernie_wallet --> wallets ``` ## Tran Codes The setup defines several `TranCodes`: - `ALLOC_BUDGET`: Allocates an amount to a specific budget, creating a credit entry in the account and a debit entry in the budget offset account. - `DEALLOC_BUDGET`: Deallocates funds from a budget, creating a credit entry in the budget offset account and a debit entry in the account. - `ASSIGN_TO_BUDGET`: Assigns a transaction to a particular budget, creating a credit entry in the account and a debit entry in the budget offset account. - `RECORD_TX`: Records a transaction between two accounts, creating a debit entry in one account and a credit entry in another. - `RECORD_PENDING_TX`: Records a pending transaction between two accounts, creating a debit entry in one account and a credit entry in another. - `RECORD_SETTLE_PENDING_TX`: Records the settlement of a pending transaction, creating a credit entry in the corresponding account and a debit entry in the settlement account. - `WALLET_TRANSFER`: Transfers funds between two wallets, creating a debit entry in one wallet and a credit entry in another. - `WALLET_DEPOSIT`: Deposits funds into a wallet, creating a debit entry in the wallet account and a credit entry in the checking account. - `WALLET_WITHDRAW`: Withdraws funds from a wallet, creating a debit entry in the checking account and a credit entry in the wallet account. Each `TranCode` defines the entries that should be created in the ledger when the transaction is processed, as well as any metadata that should be associated with the transaction. ## GraphQL **Request** ```graphql mutation SetupAccounts( $journalId: UUID! $journalIdExpression: Expression! $set_bertId: UUID! $set_budgetsId: UUID! $set_ernieId: UUID! $set_settlementId: UUID! $set_usersId: UUID! $set_walletsId: UUID! $bertBudgetId: UUID! $bertCashId: UUID! $bertCheckingId: UUID! $bertWalletId: UUID! $budgetOffsetId: UUID! $budgetOffsetIdExpression: Expression! $ernieBudgetId: UUID! $ernieCheckingId: UUID! $ernieCreditCardId: UUID! $ernieWalletId: UUID! $settlementCardId: UUID! $settlementCashId: UUID! $settlementCheckingId: UUID! $tc_allocBudgetId: UUID! $tc_deallocBudgetId: UUID! $tc_recordTxId: UUID! $tc_recordPendingTxId: UUID! $tc_recordSettlePendingTxId: UUID! $tc_assignToBudgetId: UUID! $tc_walletTransferId: UUID! $tc_walletDepositId: UUID! $tc_walletWithdrawId: UUID! ) { gl: createJournal( input: { journalId: $journalId, name: "GL", description: "General Ledger" } ) { journalId } schema { createIndex( input: { name: "TRANSACTION.BUDGET_CATEGORY" on: Transaction unique: false partition: [ { alias: "userAccountId" value: "string(document.metadata.userAccountId)" } { alias: "budgetCategory" value: "string(document.metadata.budgetCategory)" } ] sort: [ { alias: "budgetCategory" value: "string(document.metadata.budgetCategory)" sort: ASC } ] constraints: { hasCategory: "has(document.metadata.budgetCategory)" hasUserAccountId: "has(document.metadata.userAccountId)" } } ) { name on unique } } acct_set_bert: createAccountSet( input: { accountSetId: $set_bertId journalId: $journalId name: "Bert" description: "Bert's account" normalBalanceType: DEBIT } ) { accountSetId name } acct_set_budgets: createAccountSet( input: { accountSetId: $set_budgetsId journalId: $journalId name: "Budgets" description: "All budget accounts" normalBalanceType: CREDIT } ) { accountSetId name } acct_set_ernie: createAccountSet( input: { accountSetId: $set_ernieId journalId: $journalId name: "Ernie" description: "Ernie's account." normalBalanceType: DEBIT } ) { accountSetId name } acct_set_settlement: createAccountSet( input: { accountSetId: $set_settlementId journalId: $journalId name: "Settlement" description: "All settlement accounts." normalBalanceType: CREDIT } ) { accountSetId name } acct_set_users: createAccountSet( input: { accountSetId: $set_usersId journalId: $journalId name: "Users" description: "All users' accounts." normalBalanceType: DEBIT } ) { accountSetId name } acct_set_wallets: createAccountSet( input: { accountSetId: $set_walletsId journalId: $journalId name: "Wallets" description: "All customer wallets." normalBalanceType: DEBIT } ) { accountSetId name } acct_bert_budget: createAccount( input: { accountId: $bertBudgetId code: "BERT.BUDGET" name: "Bert's Budget" description: "Bert's budgeting account" normalBalanceType: CREDIT accountSetIds: [$set_bertId, $set_budgetsId] status: ACTIVE } ) { accountId code name } acct_bert_cash: createAccount( input: { accountId: $bertCashId code: "BERT.CASH" name: "Bert's Cash Acct." description: "Bert's Cash" normalBalanceType: DEBIT accountSetIds: [$set_bertId] status: ACTIVE } ) { accountId code name } acct_bert_checking: createAccount( input: { accountId: $bertCheckingId code: "BERT.CHECKING" name: "Bert's Checking Acct." description: "Bert's Checking Acct." normalBalanceType: DEBIT accountSetIds: [$set_bertId] status: ACTIVE } ) { accountId code name } acct_bert_wallet: createAccount( input: { accountId: $bertWalletId code: "BERT.WALLET" name: "Bert's Wallet" description: "Bert's wallet" normalBalanceType: DEBIT accountSetIds: [$set_bertId, $set_walletsId] status: ACTIVE } ) { accountId code name } acct_budget_offset: createAccount( input: { accountId: $budgetOffsetId code: "BUDGET.OFFSET" name: "Budget Offset" description: "Offset accounts for budgeting." normalBalanceType: CREDIT accountSetIds: [$set_budgetsId] status: ACTIVE } ) { accountId code name } acct_ernie_budget: createAccount( input: { accountId: $ernieBudgetId code: "ERNIE.BUDGET" name: "Ernie's Budget" description: "Ernie's budgeting account" normalBalanceType: CREDIT accountSetIds: [$set_ernieId, $set_budgetsId] status: ACTIVE } ) { accountId code name } acct_ernie_checking: createAccount( input: { accountId: $ernieCheckingId code: "ERNIE.CHECKING" name: "Ernie's Checking Acct." description: "" normalBalanceType: DEBIT accountSetIds: [$set_ernieId] status: ACTIVE } ) { accountId code name } acct_ernie_credit_card: createAccount( input: { accountId: $ernieCreditCardId code: "ERNIE.CREDIT_CARD" name: "Ernie's Credit Card Acct." description: "Ernie's Credit Card Acct." normalBalanceType: CREDIT accountSetIds: [$set_ernieId] status: ACTIVE } ) { accountId code name } acct_ernie_wallet: createAccount( input: { accountId: $ernieWalletId code: "ERNIE.WALLET" name: "Ernie's Wallet" description: "Ernie's wallet" normalBalanceType: DEBIT accountSetIds: [$set_ernieId, $set_walletsId] status: ACTIVE } ) { accountId code name } acct_settlement_card: createAccount( input: { accountId: $settlementCardId code: "SETTLEMENT.CARD" name: "Card Settlement" description: "Settlement account for credit card transactions" normalBalanceType: CREDIT accountSetIds: [$set_settlementId] status: ACTIVE } ) { accountId code name } acct_settlement_cash: createAccount( input: { accountId: $settlementCashId code: "SETTLEMENT.CASH" name: "Cash Settlement" description: "Settlement account for cash transactions" normalBalanceType: CREDIT accountSetIds: [$set_settlementId] status: ACTIVE } ) { accountId code name } acct_settlement_checking: createAccount( input: { accountId: $settlementCheckingId code: "SETTLEMENT.CHECKING" name: "Checking Settlement" description: "Settlement account for checking transactions" normalBalanceType: CREDIT accountSetIds: [$set_settlementId] status: ACTIVE } ) { accountId code name } tc_alloc_budget: createTranCode( input: { tranCodeId: $tc_allocBudgetId code: "ALLOC_BUDGET" description: "Allocate an amount to a specific budget." params: [ { name: "account" type: UUID description: "User's budget account ID." } { name: "amount" type: DECIMAL description: "Amount to allocate to budget." } { name: "effective", type: DATE, description: "Current date." } { name: "category" type: STRING description: "Budget category to allocate to: 'Discretionary', 'Expenses', etc." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Allocate ' + string(params.amount) + ' to budget \"' + string(params.category) + '\" for user with account ID: ' + string(params.account)" metadata: "{ 'userAccountId': string(params.account), 'budgetCategory': params.category }" } entries: [ { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'ALLOC_BUDGET_CR'" direction: "CREDIT" layer: "ENCUMBRANCE" } { accountId: $budgetOffsetIdExpression units: "params.amount" currency: "'USD'" description: "''" entryType: "'ALLOC_BUDGET_DR'" direction: "DEBIT" layer: "ENCUMBRANCE" } ] } ) { tranCodeId code } tc_dealloc_budget: createTranCode( input: { tranCodeId: $tc_deallocBudgetId code: "DEALLOC_BUDGET" description: "Deallocate funds from a budget." params: [ { name: "account" type: UUID description: "User's budget account ID." } { name: "amount" type: DECIMAL description: "Amount to deallocate from budget." } { name: "effective", type: DATE, description: "Current date." } { name: "category" type: STRING description: "Budget category to deallocate from: 'Discretionary', 'Expenses', etc." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Deallocate ' + string(params.amount) + ' to budget \"' + string(params.category) + '\" for user with account ID: ' + string(params.account)" metadata: "{ 'userAccountId': string(params.account), 'budgetCategory': params.category }" } entries: [ { accountId: $budgetOffsetIdExpression units: "params.amount" currency: "'USD'" description: "''" entryType: "'DEALLOC_BUDGET_CR'" direction: "CREDIT" layer: "ENCUMBRANCE" } { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'DEALLOC_BUDGET_DR'" direction: "DEBIT" layer: "ENCUMBRANCE" } ] } ) { tranCodeId code } tc_assign_to_budget: createTranCode( input: { tranCodeId: $tc_assignToBudgetId code: "ASSIGN_TO_BUDGET" description: "Assign a transaction to a particular budget." params: [ { name: "account" type: UUID description: "User's budget account ID." } { name: "amount" type: DECIMAL description: "Amount to assign to budget." } { name: "effective", type: DATE, description: "Current date." } { name: "category" type: STRING description: "Budget category to deallocate from: 'Discretionary', 'Expenses', etc." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Assign ' + string(params.amount) + ' to budget \"' + string(params.category) + '\" for user with account ID: ' + string(params.account)" metadata: "{ 'userAccountId': string(params.account), 'budgetCategory': params.category }" } entries: [ { accountId: $budgetOffsetIdExpression units: "params.amount" currency: "'USD'" description: "''" entryType: "'ASSIGN_TO_BUDGET_CR'" direction: "CREDIT" layer: "ENCUMBRANCE" } { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'ASSIGN_TO_BUDGET_DR'" direction: "DEBIT" layer: "ENCUMBRANCE" } ] } ) { tranCodeId code } tc_record_tx: createTranCode( input: { tranCodeId: $tc_recordTxId code: "RECORD_TX" description: "Record a transaction." params: [ { name: "account", type: UUID, description: "User's asset account ID." } { name: "settlementAccount" type: UUID description: "Settlement account ID to use, determined by the transaction type. E.g. for card transactions, use the SETTLEMENT.CARD account." } { name: "amount", type: DECIMAL, description: "Amount of transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } { name: "transactionType" type: STRING description: "Type of transaction: ACH Debit, Check Deposit, etc." } { name: "isDebit" type: BOOLEAN description: "If true (default), debit the account specified. If false, credit the account." default: true } { name: "correlationId" type: STRING description: "Correlation identifier to group transactions in this sequence." default: "_unspecified_" } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Record ' + string(params.isDebit ? 'debit' : 'credit') + ' transaction for user with account ID: ' + string(params.account)" metadata: "{ 'transactionType': string(params.transactionType) }" correlationId: "string(params.correlationId != '_unspecified_' ? params.correlationId : uuid.New())" } entries: [ { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "SETTLED" } { accountId: "params.settlementAccount" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } tc_record_pending_tx: createTranCode( input: { tranCodeId: $tc_recordPendingTxId code: "RECORD_PENDING_TX" description: "Record a pending transaction." params: [ { name: "account", type: UUID, description: "User's asset account ID." } { name: "settlementAccount" type: UUID description: "Settlement account ID to use, determined by the transaction type. E.g. for card transactions, use the SETTLEMENT.CARD account." } { name: "amount", type: DECIMAL, description: "Amount of transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } { name: "transactionType" type: STRING description: "Type of transaction: ACH Debit, Check Deposit, etc." } { name: "isDebit" type: BOOLEAN description: "If true (default), debit the account specified. If false, credit the account." default: true } { name: "correlationId" type: STRING description: "Correlation identifier to group transactions in this sequence." default: "_unspecified_" } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Record pending ' + string(params.isDebit ? 'debit' : 'credit') + ' transaction for user with account ID: ' + string(params.account)" metadata: "{ 'transactionType': string(params.transactionType) }" correlationId: "string(params.correlationId != '_unspecified_' ? params.correlationId : uuid.New())" } entries: [ { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_PENDING_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "PENDING" } { accountId: "params.settlementAccount" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_PENDING_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "PENDING" } ] } ) { tranCodeId code } tc_record_settle_pending_tx: createTranCode( input: { tranCodeId: $tc_recordSettlePendingTxId code: "RECORD_SETTLE_PENDING_TX" description: "Settle a recorded pending transaction." params: [ { name: "account", type: UUID, description: "User's asset account ID." } { name: "settlementAccount" type: UUID description: "Settlement account ID to use, determined by the transaction type. E.g. for card transactions, use the SETTLEMENT.CARD account." } { name: "settledAmount" type: DECIMAL description: "Amount of settled transaction." } { name: "originalAmount" type: DECIMAL description: "Amount of original (pending) transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } { name: "transactionType" type: STRING description: "Type of transaction: ACH Debit, Check Deposit, etc." } { name: "isDebit" type: BOOLEAN description: "If true (default), debit the account specified. If false, credit the account." default: true } { name: "correlationId" type: STRING description: "Correlation identifier to group transactions in this sequence." default: "_unspecified_" } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Settle recorded pending ' + string(params.isDebit ? 'debit' : 'credit') + ' transaction for user with account ID: ' + string(params.account)" metadata: "{ 'transactionType': string(params.transactionType) }" correlationId: "string(params.correlationId != '_unspecified_' ? params.correlationId : uuid.New())" } entries: [ { accountId: "params.account" units: "params.settledAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "SETTLED" } { accountId: "params.settlementAccount" units: "params.settledAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.originalAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_PENDING_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "PENDING" } { accountId: "params.settlementAccount" units: "params.originalAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_PENDING_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "PENDING" } ] } ) { tranCodeId code } tc_wallet_transfer: createTranCode( input: { tranCodeId: $tc_walletTransferId code: "WALLET_TRANSFER" description: "Transfer funds between wallets." params: [ { name: "fromWallet" type: UUID description: "User's wallet account to credit." } { name: "toWallet" type: UUID description: "User's wallet account to debit." } { name: "amount", type: DECIMAL, description: "Amount to transfer." } { name: "effective", type: DATE, description: "Date of transfer." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Transfer ' + string(params.amount) + ' from wallet ' + string(params.fromWallet) + ' to wallet ' + string(params.toWallet)" } entries: [ { accountId: "params.fromWallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.toWallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } tc_wallet_deposit: createTranCode( input: { tranCodeId: $tc_walletDepositId code: "WALLET_DEPOSIT" description: "Deposit funds into a user's wallet account from their checking account." params: [ { name: "wallet", type: UUID, description: "User's wallet account." } { name: "checking" type: UUID description: "User's checking account." } { name: "amount" type: DECIMAL description: "Amount to move into wallet." } { name: "effective", type: DATE, description: "Date of deposit." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Transfer ' + string(params.amount) + ' from checking account ' + string(params.checking) + ' to wallet ' + string(params.wallet)" } entries: [ { accountId: "params.checking" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.wallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } tc_wallet_withdraw: createTranCode( input: { tranCodeId: $tc_walletWithdrawId code: "WALLET_WITHDRAW" description: "Withdraw funds from a user's wallet account into their checking account." params: [ { name: "wallet", type: UUID, description: "User's wallet account." } { name: "checking" type: UUID description: "User's checking account." } { name: "amount" type: DECIMAL description: "Amount to withdraw from wallet." } { name: "effective", type: DATE, description: "Date of withdrawal." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Transfer ' + string(params.amount) + ' to checking account ' + string(params.checking) + ' from wallet ' + string(params.wallet)" } entries: [ { accountId: "params.wallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.checking" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } } ``` **Response** ```json { "data": { "gl": { "journalId": "c2881874-007e-43e1-85ef-c263e8e361aa" }, "schema": { "createIndex": { "name": "TRANSACTION.BUDGET_CATEGORY", "on": "Transaction", "unique": false } }, "acct_set_bert": { "accountSetId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "name": "Bert" }, "acct_set_budgets": { "accountSetId": "e65e8d75-5f9f-4028-bb8c-5f8270a0d2a6", "name": "Budgets" }, "acct_set_ernie": { "accountSetId": "0a13f3b3-73dd-4a88-91be-ac0737bd7175", "name": "Ernie" }, "acct_set_settlement": { "accountSetId": "d46fcd5f-8a19-4909-8bf1-7915f5f91612", "name": "Settlement" }, "acct_set_users": { "accountSetId": "afa31512-022a-41cf-b223-a261f5526510", "name": "Users" }, "acct_set_wallets": { "accountSetId": "0e4d8596-6640-40d0-8a5c-5d9c8c10e534", "name": "Wallets" }, "acct_bert_budget": { "accountId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "code": "BERT.BUDGET", "name": "Bert's Budget" }, "acct_bert_cash": { "accountId": "74f4fbe3-daee-49c9-84ce-f5361b057a3d", "code": "BERT.CASH", "name": "Bert's Cash Acct." }, "acct_bert_checking": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "acct_bert_wallet": { "accountId": "7c1afcde-7863-41b8-9688-72730f4d61f9", "code": "BERT.WALLET", "name": "Bert's Wallet" }, "acct_budget_offset": { "accountId": "75da606c-03d6-4ea3-9a03-530710981f5b", "code": "BUDGET.OFFSET", "name": "Budget Offset" }, "acct_ernie_budget": { "accountId": "18b7c23f-460e-4593-937c-ccff0db3bcca", "code": "ERNIE.BUDGET", "name": "Ernie's Budget" }, "acct_ernie_checking": { "accountId": "9c424964-d102-4610-af8a-003ddd1bf270", "code": "ERNIE.CHECKING", "name": "Ernie's Checking Acct." }, "acct_ernie_credit_card": { "accountId": "55d75807-ec03-4d09-b607-472e9263985b", "code": "ERNIE.CREDIT_CARD", "name": "Ernie's Credit Card Acct." }, "acct_ernie_wallet": { "accountId": "a13bd92a-7450-46a5-adce-d5385805fd15", "code": "ERNIE.WALLET", "name": "Ernie's Wallet" }, "acct_settlement_card": { "accountId": "685fba2a-1ec6-4ae9-ace6-d9683d142c16", "code": "SETTLEMENT.CARD", "name": "Card Settlement" }, "acct_settlement_cash": { "accountId": "9583782b-3d02-45c0-a753-76e95710431d", "code": "SETTLEMENT.CASH", "name": "Cash Settlement" }, "acct_settlement_checking": { "accountId": "be381442-bd6f-4e52-a6dd-64380ffb1f45", "code": "SETTLEMENT.CHECKING", "name": "Checking Settlement" }, "tc_alloc_budget": { "tranCodeId": "2e92e3aa-9871-4c47-8c9a-5d76e6340769", "code": "ALLOC_BUDGET" }, "tc_dealloc_budget": { "tranCodeId": "d1eaa3c4-ea60-4da3-b225-cf2955391824", "code": "DEALLOC_BUDGET" }, "tc_assign_to_budget": { "tranCodeId": "95ede9f4-b3f6-4cc3-ab18-338fd9f41e8b", "code": "ASSIGN_TO_BUDGET" }, "tc_record_tx": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX" }, "tc_record_pending_tx": { "tranCodeId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "code": "RECORD_PENDING_TX" }, "tc_record_settle_pending_tx": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX" }, "tc_wallet_transfer": { "tranCodeId": "62d8644c-ea6d-46b9-b295-98d006e5a116", "code": "WALLET_TRANSFER" }, "tc_wallet_deposit": { "tranCodeId": "8e924fb1-de38-47c4-86a8-ce93012c5711", "code": "WALLET_DEPOSIT" }, "tc_wallet_withdraw": { "tranCodeId": "499a549a-3da9-4256-a0dc-c59f69602df2", "code": "WALLET_WITHDRAW" } } } ``` **Variables** ```json { "journalId": "c2881874-007e-43e1-85ef-c263e8e361aa", "journalIdExpression": "uuid('c2881874-007e-43e1-85ef-c263e8e361aa')", "set_bertId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "set_budgetsId": "e65e8d75-5f9f-4028-bb8c-5f8270a0d2a6", "set_ernieId": "0a13f3b3-73dd-4a88-91be-ac0737bd7175", "set_settlementId": "d46fcd5f-8a19-4909-8bf1-7915f5f91612", "set_usersId": "afa31512-022a-41cf-b223-a261f5526510", "set_walletsId": "0e4d8596-6640-40d0-8a5c-5d9c8c10e534", "bertBudgetId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "bertCashId": "74f4fbe3-daee-49c9-84ce-f5361b057a3d", "bertCheckingId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "bertWalletId": "7c1afcde-7863-41b8-9688-72730f4d61f9", "budgetOffsetId": "75da606c-03d6-4ea3-9a03-530710981f5b", "budgetOffsetIdExpression": "uuid('75da606c-03d6-4ea3-9a03-530710981f5b')", "ernieBudgetId": "18b7c23f-460e-4593-937c-ccff0db3bcca", "ernieCheckingId": "9c424964-d102-4610-af8a-003ddd1bf270", "ernieCreditCardId": "55d75807-ec03-4d09-b607-472e9263985b", "ernieWalletId": "a13bd92a-7450-46a5-adce-d5385805fd15", "settlementCardId": "685fba2a-1ec6-4ae9-ace6-d9683d142c16", "settlementCashId": "9583782b-3d02-45c0-a753-76e95710431d", "settlementCheckingId": "be381442-bd6f-4e52-a6dd-64380ffb1f45", "tc_allocBudgetId": "2e92e3aa-9871-4c47-8c9a-5d76e6340769", "tc_deallocBudgetId": "d1eaa3c4-ea60-4da3-b225-cf2955391824", "tc_assignToBudgetId": "95ede9f4-b3f6-4cc3-ab18-338fd9f41e8b", "tc_recordTxId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "tc_recordPendingTxId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "tc_recordSettlePendingTxId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "tc_walletTransferId": "62d8644c-ea6d-46b9-b295-98d006e5a116", "tc_walletDepositId": "8e924fb1-de38-47c4-86a8-ce93012c5711", "tc_walletWithdrawId": "499a549a-3da9-4256-a0dc-c59f69602df2" } ``` --- # Tutorials These tutorials cover the skills needed to work with the Twisp accounting core with simple, step-by-step instructions and relevant examples. Source: https://www.twisp.com/docs/tutorials ## Foundations - [Setting Up Accounts](/docs/tutorials/setting-up-accounts): Create, modify, and delete accounts. - [Posting Transactions](/docs/tutorials/posting-transactions): Use tran codes to write to the ledger. - [Pulling Balances](/docs/tutorials/pulling-balances): Query balances for accounts and sets. - [Organizing with Account Sets](/docs/tutorials/organizing-with-account-sets): Add structure to your accounts with sets. - [Building Tran Codes](/docs/tutorials/building-tran-codes): Design transaction codes for your ledger. - [Working with Journals](/docs/tutorials/working-with-journals): Configure a multi-journal ledger. - [Utilizing Indexes](/docs/tutorials/indexes): Add indexes to query data. - [Creating Calculations](/docs/tutorials/calculations): Custom balances on additional dimensions. - [Enforcing Velocity](/docs/tutorials/velocity): Enforce velocity limits on accounts. ## Twisp 101 This tutorial walks through building an example project which offers checking, savings, and loan products. Start the tutorial: [Twisp 101](/docs/tutorials/twisp-101). --- # Creating Custom Indexes Learn how to create and use custom indexes in Twisp to efficiently query records based on specific fields, including data within the metadata object. Source: https://www.twisp.com/docs/tutorials/indexes Custom indexes allow you to define specific ways to query your ledger data, enabling efficient filtering and sorting based on fields like `accountId`, `status`, or even nested values within the `metadata` object. By the end of this tutorial, you will be able to create a custom index tailored to your querying needs and use it to retrieve records efficiently. > **Task:** > > - Design a custom index with partition and sort keys using CEL. > - Create the index using the `schema.createIndex` mutation. > - Query records efficiently using the custom index. --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Designing a Custom Index Before creating an index, you need to decide how you want to query your data. Let's design an index on the `Account` table to quickly find accounts based on their `accountId` and a `category` field within their `metadata`. Key components of an index definition: * **`on`:** The record type (table) the index applies to (e.g., `Account`). * **`name`:** A unique, human-readable name for the index (e.g., `account_metadata_category`). * **`partition`:** How data is grouped. Queries *must* filter on the partition key(s). Good partitioning spreads data evenly. We'll partition by `accountId`. * `alias`: A name for the key (e.g., `accountId`). * `value`: A [CEL expression](https://twisp.com/docs/reference/cel) referencing the `document` (the record being indexed) to get the partition value (e.g., `document.accountId`). * **`sort` (Range Key):** Defines the order *within* a partition, enabling range queries (`gte`, `lte`) and sorting. We'll sort by `metadata.category`. * `alias`: A name for the key (e.g., `category`). * `value`: A CEL expression for the sort value (e.g., `string(document.metadata.category)` - casting to string ensures predictable sorting). * `sort`: `ASC` or `DESC`. * **`constraints` (Optional):** CEL expressions that must *all* be true for a record to be included. We'll only index accounts that *have* a `metadata.category`. * Example: `{ hasCategory: "has(document.metadata.category)" }` * **`unique` (Optional):** If `true`, ensures the combination of partition and sort keys is unique for each indexed record. Defaults to `false`. For our example: * **Name:** `account_metadata_category` * **On:** `Account` * **Partition:** `alias: "accountId"`, `value: "document.account_id"` * **Sort:** `alias: "category"`, `value: "string(document.metadata.category)"`, `sort: ASC` * **Constraints:** `{ hasCategory: "has(document.metadata.category)" }` * **Unique:** `false` ## Create the Custom Index Use the `schema.createIndex` mutation to create the index defined above. ```graphql mutation CreateAccountMetadataIndex { schema { createIndex( input: { name: "account_metadata_category" on: Account unique: false partition: [ { alias: "accountId", value: "document.account_id" } ] sort: [ { alias: "category" value: "string(document.metadata.category)" sort: ASC } ] constraints: { hasCategory: "has(document.metadata.category)" } } ) { name on unique partition { alias value } range { # 'range' is the field for sort keys in the response alias value sort } constraints historical search } } } ``` ```json { "data": { "schema": { "createIndex": { "name": "account_metadata_category", "on": "Account", "unique": false, "partition": [ { "alias": "accountId", "value": "document.account_id" } ], "range": [ { "alias": "category", "value": "string(document.metadata.category)", "sort": "ASC" } ], "constraints": { "hasCategory": "has(document.metadata.category)" }, "historical": false, "search": false } } } } ``` This mutation creates the `account_metadata_category` index on the `Account` table. The response confirms the index structure, including its partition and range (sort) keys. > **Note:** > > Twisp also supports `createHistoricalIndex` to index every version of a record and `createSearchIndex` for eventually consistent full-text search capabilities. ## Query Using the Custom Index To use your new index, specify `index: { name: CUSTOM }` in your query and provide the index name and filters in the `where.custom` argument. You **must** provide an equality (`eq`) filter for all partition key aliases. You can optionally provide filters (`eq`, `gte`, `lte`, `prefix`, etc.) for sort key aliases. ```graphql query QueryUsingCustomIndex { accounts( index: { name: CUSTOM } where: { custom: { index: "account_metadata_category" # Name of the custom index partition: [ { alias: "accountId" # Partition key alias value: { eq: "a1b2c3d4-e5f6-7890-1234-567890abcdef" } } ] sort: [ { alias: "category" # Sort key alias value: { eq: "Premium" } # Filter condition on sort key } ] } } first: 10 ) { nodes { accountId name metadata } pageInfo { hasNextPage endCursor } } } ``` ```json { "data": { "accounts": { "nodes": [ { "accountId": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "name": "Premium Customer Account", "metadata": { "category": "Premium", "region": "US-West" } } // ... other matching accounts up to 10 ], "pageInfo": { "hasNextPage": false, "endCursor": "..." } } } } ``` This query efficiently retrieves `Account` records using the `account_metadata_category` index, filtering first by the `accountId` partition and then by the `category` sort key. ## Conclusion In this tutorial, you learned how to design and create a custom index using the `schema.createIndex` mutation. You saw how to define partition keys, sort keys, and constraints using CEL expressions. Finally, you learned how to leverage your custom index in queries for efficient data retrieval based on specific fields, including nested metadata values. Custom indexes are a powerful tool for optimizing query performance in Twisp. Consider creating them for your common query patterns. --- # Organizing with Account Sets In this tutorial, we'll explore how to use account sets to organize your chart of accounts. Source: https://www.twisp.com/docs/tutorials/organizing-with-account-sets With the structure provided by account sets, you can enhance your ledger with custom materialized balances and organize accounts into groups based on their purpose or function. > **Task:** > > - Create new sets with the `createAccountSet` mutation > - Add members to a set with the `addToAccountSet` mutation > - Get set data and its members with the `accountSet` query > - Update fields on a set with the `updateAccountSet` mutation > - Delete a set with the `deleteAccountSet` mutation --- ## Prerequisites Before beginning this tutorial, you should have a basic understanding of Twisp's ledger system and how transactions, accounts, and entries work. Review the [Accounting Core](/docs/accounting-core) docs for more context. If you'd like to follow along with the steps in this tutorial, you should have added accounts to your ledger. See the tutorial on [Setting Up Accounts](/docs/tutorials/setting-up-accounts). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Create an account set To create a new [AccountSet](/docs/reference/graphql/types/object#account-set), we'll use the `createAccountSet` mutation. This mutation takes several arguments: - `accountSetId`: A unique identifier for the account set. - `journalId`: The ID of the journal to which the account set belongs. - `name`: The name of the account set. - `description`: A description of the account set. - `normalBalanceType`: The normal balance to use for rolling up balances for this account set (either `DEBIT` or `CREDIT`). Let's create an account set to hold customer's accounts. We'll call it `"Customers"` and set the `normalBalanceType` to `CREDIT`: **Request** ```graphql mutation CreateAccountSet($accountSetCustomersId: UUID!, $journalGLId: UUID!) { createAccountSet( input: { accountSetId: $accountSetCustomersId journalId: $journalGLId name: "Customers" description: "All customer wallets." normalBalanceType: DEBIT } ) { accountSetId name description code } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets.", "code": "Ke8_GJexQNmYUifxYHtsqIIstZ_OUUg3g5Eq87el_FE" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` This operation will add a new account set to the ledger. Note the `DEBIT` balance type indicates that this account set is of the type that has a debit normal balance, which means that debits increase the balance and credits decrease the balance. ## Add set members To add an account to an account set, use the mutation `addToAccountSet`. The mutation takes two arguments: - `id`: Unique identifier for the account set to which the member will be added. - `member`: An `AccountSetMemberInput` object containing the unique identifier of the account or account set to be added as a member, as well as the type of member (`ACCOUNT` or `ACCOUNT_SET`). **Request** ```graphql mutation AddToAccountSet( $accountSetCustomersId: UUID! $accountCustomerAliciaId: UUID! ) { addToAccountSet( id: $accountSetCustomersId member: { memberId: $accountCustomerAliciaId, memberType: ACCOUNT } ) { accountSetId members(first: 10) { nodes { ... on Account { accountId name code } } } } } ``` **Response** ```json { "data": { "addToAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "members": { "nodes": [ { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "name": "Alicia", "code": "CUST.Alicia" } ] } } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c" } ``` The `addToAccountSet` field returns the updated account set, including its ID and the list of members. In this case, the list of members is limited to the first 10 nodes, and only the `accountId`, `name`, and `code` fields are included in the response. > **Task:** > > Try creating another account for a customer named "Bobby", then add their account to the "Customers" account set. ## Nest account sets within other sets One powerful feature of [AccountSets](/docs/reference/graphql/types/object#account-set) is that they can be nested within other sets. This allows us to create more complex structures for our chart of accounts. To nest one AccountSet within another, it's as simple as use the same `addToAccountSet` mutation, but with a `memberType` of `ACCOUNT_SET`. For example: ```graphql mutation AddToAccountSetNested( addToAccountSet( id: "" member: { memberId: " cust cal --> inac ``` ## Query members of an account set To query the members, we'll use the `accountSet` query and request the `members` field of the "Customers" set created earlier. **Request** ```graphql query GetAccountSet { accountSet(id: "29ef3f18-97b1-40d9-9852-27f1607b6ca8") { accountSetId name description members(first: 10) { nodes { ... on Account { accountId code name } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets.", "members": { "nodes": [ { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "code": "CUST.Bobby", "name": "Bobby" }, { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "code": "CUST.Alicia", "name": "Alicia" } ] } } } } ``` Note that the `members` field returns a paginated response. Because account sets can contain accounts _or_ other account sets, we can use an [inline fragment](https://graphql.org/learn/queries/#inline-fragments) to specify which fields are to be returned depending on the type. The "Customers" set only contains accounts at this point, so no fields for account sets need to be specified. > **Note:** > > If you are unfamiliar with union types in GraphQL, you can find a good summary on the official docs: [https://graphql.org/learn/schema/#union-types](https://graphql.org/learn/schema/#union-types). ## Update fields on an account set The `updateAccountSet` mutation is used to update fields of an existing account set (name, description, metadata, etc.). It takes as input the `id` of the account set to be updated and an `input` object containing the fields to update. **Request** ```graphql mutation UpdateAccountSet($accountSetCustomersId: UUID!) { updateAccountSet( id: $accountSetCustomersId input: { name: "Customer Wallets", code: "CUSTOMERS.WALLETS" } ) { accountSetId name code history(first: 2) { nodes { version name code } } } } ``` **Response** ```json { "data": { "updateAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customer Wallets", "code": "CUSTOMERS.WALLETS", "history": { "nodes": [ { "version": 3, "name": "Customer Wallets", "code": "CUSTOMERS.WALLETS" }, { "version": 2, "name": "Customers", "code": "Ke8_GJexQNmYUifxYHtsqIIstZ_OUUg3g5Eq87el_FE" } ] } } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` This mutation can be useful when there is a need to update the name of an existing account set due to changes in an organization's structure or operations. The response from the mutation can be used to verify that the update was successful and to track changes to the account set over time. ## Delete an account set The `deleteAccountSet` mutation is used to delete an existing account set. It takes as input the `id` of the account set to be deleted. **Request** ```graphql mutation DeleteAccountSet($accountSetCustomersId: UUID!) { deleteAccountSet(id: $accountSetCustomersId) { accountSetId } } ``` **Response** ```json { "data": { "deleteAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` This mutation can be useful when an account set is no longer needed or was created in error. ## Conclusion In this tutorial, we've explored how to use [AccountSets](/docs/reference/graphql/types/object#account-set) to organize accounts. We've covered how to create an account set, add members to it, nest sets within other sets, query set members, update fields of a set, and delete a set. By using account sets to organize your chart of accounts, you can create more flexible and powerful structures that better fit the needs of your business or organization. --- # Posting Transactions In this tutorial, we will learn how to post transactions using the GraphQL API. Source: https://www.twisp.com/docs/tutorials/posting-transactions > **Task:** > > - Determine a tran code to use and understand its `params` > - Post a transaction with a specified tran code using the `postTransaction` mutation > - Query transactions to inspect entries written with the `transaction` query Posting transactions in Twisp is a simple process that involves specifying the transaction details and the transaction code (tran code) to use. Transactions are written to the ledger, and entries are created for as specified by the tran code used. --- ## Prerequisites Before you start, you should have added accounts to your ledger. See the tutorial on [Setting Up Accounts](/docs/tutorials/setting-up-accounts). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Choose a transaction code You will need to choose a transaction code to use before posting transactions. Transaction codes are pre-defined templates that specify how transactions should be processed. Transaction codes define the accounts and amounts to debit and credit, the currency to use, and any other parameters required by the transaction. Transaction codes are usually created during the configuration of a Twisp ledger and can be customized to meet the specific needs of your organization. For this tutorial, we will use a `BOOK_TRANSFER` transaction code. The `BOOK_TRANSFER` transaction code requires the following parameters: - the account to _debit_ - the account to _credit_ - the _amount_ to transfer - the _currency_ of the transfer - the _effective date_ of the transfer transaction For a more in-depth look at this tran code, review the tutorial page on [Building Tran Codes](/docs/tutorials/building-tran-codes). Once a transaction code has been selected, a transaction can be written using the `postTransaction` mutation. ## Write a `postTransaction` mutation To post a transaction, you will need to provide the following inputs: - `transactionId`: a unique identifier for the transaction - `tranCode`: the transaction code to use - `params`: a set of key-value parameters as specified by the transaction code Once the transaction is posted, Twisp will write the transaction to the ledger and create entries for each account affected by the transaction. ### Provide a unique `transactionId` It's important to provide a unique `transactionId` when posting a transaction. This can help prevent duplicate transactions from being posted accidentally, i.e. it ensures idempotent transactions. ### Specify values for all `params` Make sure to specify all required values in the `params` object for the transaction code you are using. Here's an example mutation showing how to post a transaction with the `BOOK_TRANSFER` tran code: **Request** ```graphql mutation PostTransaction( $accountCustomerAliciaId: UUID! $accountCustomerBobbyId: UUID! ) { postTransaction( input: { transactionId: "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" tranCode: "BOOK_TRANSFER" params: { crAccount: $accountCustomerAliciaId drAccount: $accountCustomerBobbyId amount: "1.00" currency: "USD" effective: "2022-09-08" } } ) { transactionId } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" } } } ``` **Variables** ```json { "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "accountCustomerBobbyId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" } ``` This code example shows a transaction that transfers funds between two accounts. The `params` object specifies all required values for this transaction, including the customer account to credit (`crAccount`) and customer account to debit (`drAccount`), the amount to transfer (`amount`), the currency (`currency`), and the effective date of the transaction (`effective`). Keep in mind that transactions are processed according to the rules defined in the transaction code. It is important to ensure that the transaction code and its associated parameters are correct before posting the transaction. ## Query transactions to see entries written Any posted transaction can be queried to view its fields and the entries that were written to the ledger. Here's an example query: **Request** ```graphql query GetTransaction { transaction(id: "434696a7-56e5-4e14-a97a-884820690a22") { description effective tranCode { code } journal { name } metadata entries(first: 10) { nodes { sequence entryType layer direction account { name } amount { formatted(as: { locale: "en-US" }) } } } } } ``` **Response** ```json { "data": { "transaction": { "description": "Exchange 2.37 USD for EUR at a 1:0.85 exchange rate.", "effective": "2022-09-11", "tranCode": { "code": "EXCHANGE_CURRENCY" }, "journal": { "name": "GL" }, "metadata": { "buy": "EUR", "rate": "0.85", "sell": "USD" }, "entries": { "nodes": [ { "sequence": 0, "entryType": "FX_SELL_DR", "layer": "SETTLED", "direction": "CREDIT", "account": { "name": "Alicia" }, "amount": { "formatted": "$2.37" } }, { "sequence": 1, "entryType": "FX_SELL_CR", "layer": "SETTLED", "direction": "DEBIT", "account": { "name": "Foreign Exchange Broker" }, "amount": { "formatted": "$2.37" } }, { "sequence": 2, "entryType": "FX_BUY_DR", "layer": "SETTLED", "direction": "CREDIT", "account": { "name": "Foreign Exchange Broker" }, "amount": { "formatted": "€2.0145" } }, { "sequence": 3, "entryType": "FX_BUY_CR", "layer": "SETTLED", "direction": "DEBIT", "account": { "name": "Alicia" }, "amount": { "formatted": "€2.0145" } } ] } } } } ``` This GraphQL query returns a transaction with its description, effective date, transaction code, journal name, metadata, and entries. The entries include the sequence, entry type, layer, direction, account name, and formatted amount. ## Conclusion Posting transactions in Twisp is a straightforward process that requires specifying the transaction code to use along with the values for all parameters as defined by the tran code. The `postTransaction` mutation will return the posted transaction. Transactions can also be queried using the `transaction` and `transactions` queries. --- # Pulling Balances In this tutorial, we will learn how to query account balances using the GraphQL API. Source: https://www.twisp.com/docs/tutorials/pulling-balances Twisp provides a robust GraphQL schema that allows users to query account balances using a variety of filters and indexes. > **Task:** > > - Get a single balance for a specific account and currency with the `balance` query > - Get a set of balances for a given set of conditions with the `balances` query > - Pull the balance for an account with the `Account.balance` field > - Pull an aggregate balance for accounts in a set with the `AccountSet.balance` field --- ## Prerequisites Before you start, you should have added accounts to your ledger and posted some transactions. See the tutorials on [Setting Up Accounts](/docs/tutorials/setting-up-accounts) and [Posting Transactions](/docs/tutorials/posting-transactions). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ### Retrieve an account UUID First, you will need to retrieve the UUID for the account that you want to query. You can do this by querying for the account using the `accounts` query, and specifying any relevant filters. For example, to retrieve the UUID for an account with the code `CHECKING`, you can use the following GraphQL query: ```graphql query { accounts( index: { name: CODE } where: { code: { eq: "CHECKING" } } first: 1 ) { nodes { accountId name } } } ``` This query will return the UUID and name for any accounts with the code `CHECKING`. ## Query the account balance Once you have the UUID for the account that you want to query, you can use the `balance` query to retrieve the balance for that account. The `balance` query takes two arguments: - `journalId` (required): the UUID of the journal to retrieve the balance from. - `currency` (required): the currency code of the balance to retrieve. For example, to retrieve the settled normal balance for the account with UUID `"3ea12e45-7df2-4293-9434-feb792affc91"` in journal with UUID `"4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b"`, you can use the following GraphQL query: ```graphql query { balance( accountId: "3ea12e45-7df2-4293-9434-feb792affc91" journalId: "4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b" currency: "USD" ) { settled { normalBalance { units } } } } ``` This query will return the USD settled normal balance amount in decimal units for for the account given in the specified journal. ## Query multiple balances If you want to retrieve multiple balances at once, you can use the `balances` query instead. The `balances` query takes the same arguments as the `balance` query, but also allows you to specify additional filters to narrow down the results. For example, to retrieve all balances for the account with UUID `"3ea12e45-7df2-4293-9434-feb792affc91"` in currency `"USD"`, you can use the following GraphQL query: ```graphql query { balances( index: { name: ACCOUNT_ID } where: { accountId: { eq: "3ea12e45-7df2-4293-9434-feb792affc91" } } first: 5 ) { nodes { journalId currency settled { normalBalance { units } } } } } ``` This query retrieves information about the balances of a specific account. It requests the first five balances for the account with an `accountId` of `"3ea12e45-7df2-4293-9434-feb792affc91"`, and returns the `journalId`, `currency`, and `normalBalance` for each balance on the account. ## Query a balance using the `Account.balance` field To query an account balance using the `balance` field on an `Account` type, you will need to provide the `journalId` and `currency` arguments. The `journalId` argument specifies the ID of the journal for the balance, and the `currency` argument specifies the currency of the balance. For example, to retrieve the USD settled normal balance for the account with UUID `"3ea12e45-7df2-4293-9434-feb792affc91"` in journal with UUID `"4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b"`, you can use the following GraphQL query: ```graphql query { account(id: "3ea12e45-7df2-4293-9434-feb792affc91") { balance(journalId: "4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b", currency: "USD") { settled { normalBalance { units } } } } } ``` Note that this is effectively the same as using the `balance` query above: it will return the account's USD settled normal balance amount in decimal units in the specified journal. > **Note:** > > You can also query for multiple balances on accounts using the `Account.balances` field. This can be useful for accounts that contain entries across multiple journals, or for accounts that contain multiple currencies. ## Query a balance for an account set To query balances for an account set, you can use the `balances` query with the `accountSetId` argument. Balances for an account set are calculated by summing the journal balances of all accounts that are members of the account set. In this context, the journal balance for an account is the balance for all entries posted to the journal that the account set is tied to. So an account set will only show balances for entries posted to the account set's journal. For example, to retrieve the USD balance for an account set with UUID `"d1a2e7e9-7a6d-4d82-bf6c-2c8d91b1c1f6"`, you can use the following GraphQL query: ```graphql query { accountSet(id: "d1a2e7e9-7a6d-4d82-bf6c-2c8d91b1c1f6") { balance(currency: "USD") { settled { normalBalance { units } } } } } ``` This query retrieves the USD balance of the account set with UUID `"d1a2e7e9-7a6d-4d82-bf6c-2c8d91b1c1f6"`. The `currency` argument in the balance field specifies the currency to use for the balance calculation. The query includes the `normalBalance` field of the `settled` balance layer, which returns the decimal units for the normal balance of the account set. > **Note:** > > You can also query for multiple balances on account sets using the `AccountSet.balances` field. This can be useful for account sets that track accounts across multiple currencies. ## Conclusion Twisp's GraphQL schema provides a powerful set of tools for querying account balances. By leveraging the `balance` and `balances` queries, you can quickly and easily retrieve the balance information you need for your ledger entries and financial reporting. --- # Setting Up Accounts In this tutorial, we will learn how to manage accounts using the GraphQL API. Source: https://www.twisp.com/docs/tutorials/setting-up-accounts Managing financial accounts is a crucial aspect of many applications. By the end of this tutorial, you will have a solid understanding of how to manage accounts using the Twisp GraphQL API and be ready to incorporate this functionality into your own applications. > **Task:** > > - Create a new account using the `createAccount` mutation > - Update fields on an existing account using the `updateAccount` mutation > - Delete (lock) an account using the `deleteAccount` mutation --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Create an account The `createAccount` mutation is used to create new accounts with the given `accountId`, `name`, `code`, `description`, `normalBalanceType`, and `status`. The `accountId` is a unique identifier for the account that is being created, and it is required as an input field for this mutation. The `name` input field specifies the name for the new account that is being created. The `code` input field is a shorthand code for the account, which is often an abbreviated version of the account name. The `description` input field is a brief explanation of the account that is being created. The `status` input field represents the current status for the new account that is being created. This field specifies whether the account is active or closed (locked). By default, all accounts are `ACTIVE`. When an account is `LOCKED`, it cannot be updated unless you are also changing its status back to `ACTIVE`. Any attempt to write a ledger entry to a locked account will raise an error. **Request** ```graphql mutation CreateAccount($accountCardSettlementId: UUID!) { createAccount( input: { accountId: $accountCardSettlementId name: "Card Settlement" code: "SETTLE.CARD" description: "Settlement account for card transactions." normalBalanceType: CREDIT status: ACTIVE } ) { accountId name code description normalBalanceType } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "name": "Card Settlement", "code": "SETTLE.CARD", "description": "Settlement account for card transactions.", "normalBalanceType": "CREDIT" } } } ``` **Variables** ```json { "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` This `createAccount` operation returns the newly created account's `accountId`, `name`, `code`, `description`, and `normalBalanceType`. ## Update an account To update an existing account, we can use the `updateAccount` mutation. We need to provide the `id` of the account we want to update, as well as the fields we want to update. Here's an example mutation: **Request** ```graphql mutation UpdateAccount($accountCardSettlementId: UUID!) { updateAccount(id: $accountCardSettlementId, input: { code: "CARD.SETTLE" }) { accountId code history(first: 2) { nodes { version code } } } } ``` **Response** ```json { "data": { "updateAccount": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "code": "CARD.SETTLE", "history": { "nodes": [ { "version": 2, "code": "CARD.SETTLE" }, { "version": 1, "code": "SETTLE.CARD" } ] } } } } ``` **Variables** ```json { "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` This is a GraphQL mutation operation that allows the user to update an account's code. The mutation is called `updateAccount` and takes in two arguments: `id` and `input`. The `id` argument is a unique identifier for the account being updated, and the `input` argument contains the updates to apply to the account. In this specific example, the `id` argument is passed in with the `$accountCardSettlementId` variable. The response from this mutation includes several fields. The `accountId` field returns the unique identifier for the updated account, while the `code` field returns the updated code for the account. Additionally, the `history` field returns the two most recent versions of the account, along with their corresponding `version` and `code`. This allows the user to track the changes made to the account over time. Overall, this mutation provides a flexible and powerful way to update accounts in the Twisp system, and the response includes valuable information that can be used to track changes and ensure data integrity. ## Delete an account Because Twisp is an immutable database, we cannot fully "delete" an account. Deleting an account in Twisp instead marks its status as `LOCKED`, meaning that no entries can be posted to the account. A locked account can only be updated to change its status back to `ACTIVE`. To delete (lock) an account, we can use the `deleteAccount` mutation. We need to provide the `id` of the account we want to delete. Here's an example mutation: **Request** ```graphql mutation DeleteAccount($accountCustomerBobbyId: UUID!) { deleteAccount(id: $accountCustomerBobbyId) { accountId status } } ``` **Response** ```json { "data": { "deleteAccount": { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "status": "LOCKED" } } } ``` **Variables** ```json { "accountCustomerBobbyId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" } ``` Once the account is deleted, this mutation returns two fields: `accountId` and `status`. `accountId` is a UUID that uniquely identifies the deleted account, while `status` is the current status of the account. This mutation is useful if you need to remove an account that is no longer needed or was created in error. By deleting the account, you can ensure that it is no longer used in any future transactions or reports. ## Conclusion In this tutorial, we explored how to create, update, and delete accounts using the Twisp GraphQL API. We saw how to use the `createAccount`, `updateAccount`, and `deleteAccount` mutations to perform these operations. With this knowledge, you can now begin building your own applications that use the Twisp API to manage financial accounts. --- # Twisp 101 Tutorial Work through the steps in this tutorial to learn how to start building on the Twisp accounting core. Source: https://www.twisp.com/docs/tutorials/twisp-101 Welcome to the Twisp 101 Tutorial! ## Context This tutorial is designed to walk you through the foundational features of the Twisp Accounting Core. The example project we'll be using is an imaginary neobank called "Zuzu" which offers checking, savings, and loan products to under-banked customers in the US. We'll work through the steps needed to **set up a chart of accounts**, orchestrate the types of financial activity by **defining transaction codes**, and **write some ledger entries** by posting transactions. Most new Twisp customers will have a similar process regardless of the specifics of their product. The fundamental stages are: 1. **Design** the elements which give a structure and meaning to the accounting system. 2. **Test** the design to ensure that it satisfies working requirements. 3. **Deploy** products backed by the Twisp core. 4. **Monitor** for performance, usage, auditing, and reporting. In this tutorial, we'll focus on the Design and Test stages. ## The Project _Zuzu_ is a new neobank offering personal banking services: checking, savings, and loans. They work with multiple partner banks, issue debit cards, and provide online and mobile banking applications. Building on Twisp's accounting core, Zuzu has the following project requirements. - A chart of accounts to model both internal company accounts as well as customer accounts, including: - Checking accounts - Savings accounts - Loans issued - A double-entry ledger for recording financial activity, including common transactions like: - Deposits & withdrawals - Direct transfers between customers - Fees charged for a variety of reasons In the interest of getting something working, we'll start simple and just implement the accounting components needed to support the **checking** product and its primary transactions. ## Step 0: Establish API Connection Twisp is an API-first product. Before we can do anything, we need to connect to the API. ### Connect to GraphQL API When you are invited to Twisp, you will receive an account to access the Twisp Console. The console provides a [GraphiQL](https://github.com/graphql/graphiql) interface for interacting with the GraphQL API for your provisioned accounting core. This is the quickest and easiest way to get connected. Once you have proper auth credentials, you can connect to the GraphQL API from any client. ### Inspect the Schema The GraphiQL interface provides built-in docs, so you can explore the schema and read documentation about queries, mutations, and types. Let's confirm that you're connection is working. Run an introspection query directly: **Introspection** ```graphql query { __schema { types { name kind description } } } ``` You should get a large response with all the various types like `Account`, `Entry`, etc. --- # Step 1: Create a Primary Journal Before we begin designing accounts, we need to define a journal within which to organize transactions. Source: https://www.twisp.com/docs/tutorials/twisp-101/step-1 In most cases, a single [Journal](/docs/reference/graphql/types/object#journal) is enough. For Zuzu, we'll create a single "General Ledger" journal which will record all transactions. **Request** ```graphql mutation CreateGeneralLedger { createJournal( input: { journalId: "822cb59f-ce51-4837-8391-2af3b7a5fc51" name: "General Ledger" description: "Primary journal for Zuzu." } ) { journalId name description status } } ``` **Response** ```json { "data": { "createJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "General Ledger", "description": "Primary journal for Zuzu.", "status": "ACTIVE" } } } ``` > **Note:** > > Note that Twisp ledgers come with a pre-built "Default" journal, which is ideal for single-journal ledgers. For the purposes of illustration, we'll be using a custom Journal in this tutorial. Now that we have a journal, we can start building the components needed to support the core use cases. Let's start by modeling **deposits** and **withdrawals**. --- # Step 2: Model Deposits and Withdrawals The most basic transaction types Zuzu needs to support are to allow customers to move money in and out of their checking account. Source: https://www.twisp.com/docs/tutorials/twisp-101/step-2 In other words, we need to support deposits and withdrawals: a customer needs to be able to deposit money _into_ their checking account and withdraw some or all of their balance _out of_ their account. Our ledger will support ACH debit and credit transaction types to model "withdrawals" and "deposits". To build out this feature we'll need to do three things: - Create some customer [Accounts](/docs/reference/graphql/types/object#account) to model money Zuzu holds on behalf of a customer. - Create an assets account to model Zuzu's cash assets. - Design two [TranCodes](/docs/reference/graphql/types/object#tran-code) to use as a templates for ACH transactions. With double-entry accounting, every transaction needs to write _at least_ two entries to the ledger which balance out across debits and credits. *How* (with what metadata) and *where* (to which accounts) these entries are written to is determined by the type of transaction. In Twisp, transaction types are explicitly defined during the design stage by creating transaction codes, or TranCodes. > **Tip:** > > When a customer deposits money into their account, Zuzu is effectively acting as a custodian of the customer's money. This is why customer accounts are treated as a liability for the company – they represent money that Zuzu _owes_ the customer. > > The assets account represents the cash on hand that Zuzu holds at any given time. ## Create accounts First, let's create checking accounts for some sample customers. We can do this with the `createAccount` mutation. **Request** ```graphql mutation CreateCustomerAccounts { ernie_checking: createAccount( input: { accountId: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" name: "Ernie Bishop - Checking" code: "ERNIE.CHECKING" description: "Ernie's checking account" normalBalanceType: CREDIT } ) { accountId name } bert_checking: createAccount( input: { accountId: "6c6affb0-5cf5-402b-8d84-01bfc1624a2c" name: "Bert - Checking" code: "BERT.CHECKING" description: "Bert's checking account" normalBalanceType: CREDIT } ) { accountId name } } ``` **Response** ```json { "data": { "ernie_checking": { "accountId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "name": "Ernie Bishop - Checking" }, "bert_checking": { "accountId": "6c6affb0-5cf5-402b-8d84-01bfc1624a2c", "name": "Bert - Checking" } } } ``` Note that customer accounts use a credit-normal balance type because they represent liabilities. Next, let's create the assets account using a debit-normal balance type: **Request** ```graphql mutation CreateAssetsAccount { createAccount( input: { accountId: "78551b96-9c34-46f9-8d5f-c86e4459fcd7" name: "Assets" code: "ASSET" description: "Zuzu's assets (e.g. cash deposits)" normalBalanceType: DEBIT } ) { accountId name normalBalanceType } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "78551b96-9c34-46f9-8d5f-c86e4459fcd7", "name": "Assets", "normalBalanceType": "DEBIT" } } } ``` ## Check account balances Every account starts with a zero/null balance. We can check the balances of each account by querying the account id and pulling out the account balance for the primary journal we created earlier. Note that in this example, we use GraphQL variables to store the values used previously and inject them via query params. This makes it easier to re-use values across multiple requests. **Request** ```graphql query CheckAccountBalances( $ernieId: UUID! $bertId: UUID! $assetsId: UUID! $journalId: UUID! ) { ernie: account(id: $ernieId) { name balance(journalId: $journalId) { settled { normalBalance { units } } } } bert: account(id: $bertId) { name balance(journalId: $journalId) { settled { normalBalance { units } } } } assets: account(id: $assetsId) { name balance(journalId: $journalId) { settled { normalBalance { units } } } } } ``` **Response** ```json { "data": { "ernie": { "name": "Ernie Bishop - Checking", "balance": null }, "bert": { "name": "Bert - Checking", "balance": null }, "assets": { "name": "Assets", "balance": null } } } ``` **Variables** ```json { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "ernieId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "bertId": "6c6affb0-5cf5-402b-8d84-01bfc1624a2c", "assetsId": "78551b96-9c34-46f9-8d5f-c86e4459fcd7" } ``` Just as expected - balances are `null` for all accounts. Not very exciting. Let's change that. ## Design the transaction type as a TranCode The only way to write ledger entries in Twisp is by **posting a transaction**. Furthermore, every transaction is structured by the tran code used. This ensures that the ledger is consistent, predictable, and correct. To define the tran codes for ACH credits and debit transaction types, we need to first determine: - A unique identifier code for the tran code - Which accounts will be debited/credited - What entry data will be written - How we will create parameterize inputs (for values like the amount) Let's keep it simple and use the codes `ACH_CREDIT` and `ACH_DEBIT` for these tran codes. For deposits (i.e. ACH credits), we'll **credit** the customer's checking account because this account is credit-normal and represents Zuzu's obligation to the customer, and we'll **debit** the assets account because this is the debit-normal account which represents how much liquid currency Zuzu has on hand (in this case, on behalf of the customer). Withdrawals (i.e. ACH debits) are going to be basically the same, but reversed: debit the customer's checking and credit the assets account. We'll write one entry for the debit and one for the credit, using an entry type to clarify the function of the entry within the context of the transaction. Finally, we'll need to parameterize both the amount as well as the customer's checking account ID, since these are the salient pieces of information that we want to be able to provide when posting a transaction using this tran code. ### Create the TranCodes for DEPOSIT and WITHDRAW Now we can create these tran codes with GraphQL, plugging in the design decisions we just made to encode these transaction types. **Request** ```graphql mutation CreateDepositAndWithdrawalTranCodes($achCrId: UUID!, $achDrId: UUID!) { achCredit: createTranCode( input: { tranCodeId: $achCrId code: "ACH_CREDIT" description: "An ACH credit into a customer account." params: [ { name: "account", type: UUID, description: "Deposit account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "effective" type: DATE description: "Effective date for transaction." } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effective" } entries: [ { accountId: "uuid('78551b96-9c34-46f9-8d5f-c86e4459fcd7')" units: "params.amount" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } achDebit: createTranCode( input: { tranCodeId: $achDrId code: "ACH_DEBIT" description: "An ACH debit into a customer account." params: [ { name: "account", type: UUID, description: "Withdraw account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "effective" type: DATE description: "Effective date for transaction." } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effective" } entries: [ { accountId: "uuid('78551b96-9c34-46f9-8d5f-c86e4459fcd7')" units: "params.amount" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "achCredit": { "tranCodeId": "45f3f5da-034e-40c1-aaff-ab6d01bd446f" }, "achDebit": { "tranCodeId": "fab492ae-2fe4-4fcd-9bf7-cf06eb5f796b" } } } ``` **Variables** ```json { "achCrId": "45f3f5da-034e-40c1-aaff-ab6d01bd446f", "achDrId": "fab492ae-2fe4-4fcd-9bf7-cf06eb5f796b" } ``` ## Post a test transaction With these tran codes defined, we can now post transactions using them. Let's deposit $9.53 into Ernie's account: **Request** ```graphql mutation PostDeposit { postTransaction( input: { transactionId: "42847c7f-1972-4448-91b7-652c378760f4" tranCode: "ACH_CREDIT" params: { account: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" amount: "9.53" effective: "2022-09-21" } } ) { transactionId tranCodeId effective entries(first: 2) { nodes { units direction account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "42847c7f-1972-4448-91b7-652c378760f4", "tranCodeId": "45f3f5da-034e-40c1-aaff-ab6d01bd446f", "effective": "2022-09-21", "entries": { "nodes": [ { "units": "9.53", "direction": "DEBIT", "account": { "name": "Assets" } }, { "units": "9.53", "direction": "CREDIT", "account": { "name": "Ernie Bishop - Checking" } } ] } } } } ``` That all looks good. Now let's withdraw $4.28 from Ernie's account: **Request** ```graphql mutation PostACHWithdrawal { postTransaction( input: { transactionId: "39d2288d-96f9-40c7-b587-e7e75df083fa" tranCode: "ACH_DEBIT" params: { account: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" amount: "4.28" effective: "2022-09-21" } } ) { transactionId tranCodeId effective entries(first: 2) { nodes { units direction account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "39d2288d-96f9-40c7-b587-e7e75df083fa", "tranCodeId": "fab492ae-2fe4-4fcd-9bf7-cf06eb5f796b", "effective": "2022-09-21", "entries": { "nodes": [ { "units": "4.28", "direction": "CREDIT", "account": { "name": "Assets" } }, { "units": "4.28", "direction": "DEBIT", "account": { "name": "Ernie Bishop - Checking" } } ] } } } } ``` Great! We've posted our first transactions. Let's go to the next feature. --- # Step 3: Model Intra-Bank Transfers Zuzu also needs to support bank transfers between customer accounts so that customers can send money to one another. Source: https://www.twisp.com/docs/tutorials/twisp-101/step-3 This type of transaction we'll model with a tran code called `BANK_TRANSFER`, but it's going to be a little more complex than the previous one. Zuzu isn't just letting customers transfer money for free, all day long. Instead, they'll charge a small percentage fee of 1% with a $10 maximum, paid by the sender. ## Create a revenue account The fee charged for a bank transfer will be represented as a `DEBIT` against the sender's bank account. The balancing `CREDIT` entry will be posted to Zuzu's revenue account, which doesn't exist yet. To support bank transfers, then, we'll need to first create the revenue account for Zuzu. **Request** ```graphql mutation CreateRevenueAccount($revenueId: UUID!) { createAccount( input: { accountId: $revenueId name: "Revenues" code: "REV" description: "Company revenues (e.g. fees)" normalBalanceType: CREDIT } ) { accountId name } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "ece5e752-5445-4f4e-8861-d09c5c417061", "name": "Revenues" } } } ``` **Variables** ```json { "revenueId": "ece5e752-5445-4f4e-8861-d09c5c417061" } ``` Now that we have the revenue account, we can design the tran code for internal transfers. ## Define the TranCode for transfers The tran code for this transaction type needs to do a few things: - Write entries to move the defined amount from the sender's checking account to the receiver's checking account - Write an additional two entries to move the fee from the sender's checking account to the Revenue account, using a runtime expression to calculate the fee amount When posting a transaction, we want to enable the poster to provide the **sender's** account ID, the **receiver's** account ID, the **amount** to transfer, the 1% **fee**, and the **effective date** of the transfer. We'll define each of these as `params` on the TranCode. **Request** ```graphql mutation CreateBankTransferTranCode($transferId: UUID!) { createTranCode( input: { tranCodeId: $transferId code: "BANK_TRANSFER" description: "Transfer $ internally from one checking account to another. The sender is charged a 1% fee or $10, whichever is smaller." params: [ { name: "fromAccount", type: UUID, description: "Sender's account ID." } { name: "toAccount", type: UUID, description: "Receiver's account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "fee" type: DECIMAL description: "Transfer fee as decimal percentage, e.g. `0.01` for 1%" } { name: "effective" type: DATE description: "Effective date for transaction." } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effective" } entries: [ { accountId: "uuid(params.fromAccount)" units: "params.amount" currency: "'USD'" entryType: "'TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "uuid(params.toAccount)" units: "params.amount" currency: "'USD'" entryType: "'TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "uuid(params.fromAccount)" units: "decimal.Round(decimal.Mul(params.amount, params.fee), 'half_up', 2)" currency: "'USD'" entryType: "'TRANSFER_FEE_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "uuid('ece5e752-5445-4f4e-8861-d09c5c417061')" # This is the account ID for the Revenues account units: "decimal.Round(decimal.Mul(params.amount, params.fee), 'half_up', 2)" currency: "'USD'" entryType: "'TRANSFER_FEE_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "a0d9e35a-1df6-4f22-8e39-15c72e60b2d5" } } } ``` **Variables** ```json { "transferId": "a0d9e35a-1df6-4f22-8e39-15c72e60b2d5" } ``` > **Tip:** > > Writing clear descriptions for tran codes and their parameters is a great way to help API users can understand what the tran code is for and how to invoke it. ## Post a test transaction Let's test this transaction out by sending $2.25 from Ernie to Bert. To see the results of the transaction as encoded by the tran code, we'll return the entries posted, digging all the way down into the account for each entry. **Request** ```graphql mutation PostBankTransfer { postTransaction( input: { transactionId: "9c328550-bba3-423b-a58a-b3f9786a80ae" tranCode: "BANK_TRANSFER" params: { fromAccount: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" toAccount: "6c6affb0-5cf5-402b-8d84-01bfc1624a2c" amount: "2.25" fee: "0.02" effective: "2022-09-10" } } ) { transactionId entries(first: 10) { nodes { units direction entryType account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "9c328550-bba3-423b-a58a-b3f9786a80ae", "entries": { "nodes": [ { "units": "2.25", "direction": "DEBIT", "entryType": "TRANSFER_DR", "account": { "name": "Ernie Bishop - Checking" } }, { "units": "2.25", "direction": "CREDIT", "entryType": "TRANSFER_CR", "account": { "name": "Bert - Checking" } }, { "units": "0.05", "direction": "DEBIT", "entryType": "TRANSFER_FEE_DR", "account": { "name": "Ernie Bishop - Checking" } }, { "units": "0.05", "direction": "CREDIT", "entryType": "TRANSFER_FEE_CR", "account": { "name": "Revenues" } } ] } } } } ``` Success! From our response, we can see that each entry was posted to the correct account and for the correct amount. --- # Step 4: Organize Accounts for Balance Rollups Use account sets to group and organize accounts into a chart, taking advantage of the built-in balance aggregations. Source: https://www.twisp.com/docs/tutorials/twisp-101/step-4 With [AccountSets](/docs/reference/graphql/types/object#account-set), we can collect related accounts to provide an easy interface into summary balances and queries into the entries. We've already created checking accounts for each customer, but Zuzu also needs a way to summarize _all_ customer accounts so that we can see the total balance. To accomplish this, we'll create an account set called "Customers" and add the customer accounts to it. ## Create customers account set **Request** ```graphql mutation CreateCustomersAccountSet($customersId: UUID!, $journalId: UUID!) { createAccountSet( input: { accountSetId: $customersId journalId: $journalId name: "Customers" description: "All customer's accounts" normalBalanceType: CREDIT } ) { accountSetId journalId name description normalBalanceType } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "a6ee5252-a8db-4fdc-960d-64970f3385ab", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "Customers", "description": "All customer's accounts", "normalBalanceType": "CREDIT" } } } ``` **Variables** ```json { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "customersId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } ``` Now that we have an account set, let's add the two customer accounts to it: **Request** ```graphql mutation AddCustomersToSet( $customersId: UUID! $ernieId: UUID! $bertId: UUID! ) { addErnie: addToAccountSet( id: $customersId member: { memberType: ACCOUNT, memberId: $ernieId } ) { accountSetId } addBert: addToAccountSet( id: $customersId member: { memberType: ACCOUNT, memberId: $bertId } ) { accountSetId } } ``` **Response** ```json { "data": { "addErnie": { "accountSetId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" }, "addBert": { "accountSetId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } } } ``` **Variables** ```json { "ernieId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "bertId": "6c6affb0-5cf5-402b-8d84-01bfc1624a2c", "customersId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } ``` Now that we have posted several transactions and created an account set, we can look at balances and interrogate their history to see how account balances change with each activity posted to that account. ## Review balances & interrogate history Let's start by querying for the balance of the "Customers" set, as well as the balance of each member account. **Request** ```graphql query GetCustomersBalances($customersId: UUID!, $journalId: UUID!) { accountSet(id: $customersId) { name balance { settled { normalBalance { units } } } members(first: 10) { nodes { __typename ... on Account { name balance(journalId: $journalId) { settled { normalBalance { units } } } } } } } } ``` **Response** ```json { "data": { "accountSet": { "name": "Customers", "balance": { "settled": { "normalBalance": { "units": "5.20" } } }, "members": { "nodes": [ { "__typename": "Account", "name": "Bert - Checking", "balance": { "settled": { "normalBalance": { "units": "2.25" } } } }, { "__typename": "Account", "name": "Ernie Bishop - Checking", "balance": { "settled": { "normalBalance": { "units": "2.95" } } } } ] } } } } ``` **Variables** ```json { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "customersId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } ``` As expected, the account set's balance is always equal to the sum of its member's balances. Because records in Twisp are append-only, we can review the history of any record to see how its state changed over time. Let's query the balance history of Ernie's account and compare it to the entries to see how it changed in response to transactions posted. **Request** ```graphql query GetErnieBalanceHistory($ernieId: UUID!, $journalId: UUID!) { account(id: $ernieId) { name balance(journalId: $journalId) { settled { normalBalance { units } } version history(first: 10) { nodes { version settled { normalBalance { units } } } } } entries( where: { journalId: { eq: "822cb59f-ce51-4837-8391-2af3b7a5fc51" } } first: 10 ) { nodes { entryType direction units } } } } ``` **Response** ```json { "data": { "account": { "name": "Ernie Bishop - Checking", "balance": { "history": { "nodes": [ { "settled": { "normalBalance": { "units": "2.95" } }, "version": 4 }, { "settled": { "normalBalance": { "units": "3.00" } }, "version": 3 }, { "settled": { "normalBalance": { "units": "5.25" } }, "version": 2 }, { "settled": { "normalBalance": { "units": "9.53" } }, "version": 1 } ] }, "settled": { "normalBalance": { "units": "2.95" } }, "version": 4 }, "entries": { "nodes": [ { "direction": "DEBIT", "entryType": "TRANSFER_FEE_DR", "units": "0.05" }, { "direction": "DEBIT", "entryType": "TRANSFER_DR", "units": "2.25" }, { "direction": "DEBIT", "entryType": "ACH_DR", "units": "4.28" }, { "direction": "CREDIT", "entryType": "ACH_CR", "units": "9.53" } ] } } } } ``` **Variables** ```json { "ernieId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## Conclusion That finalizes the tutorial! Let's recap what we did to customize and test an accounting core for the imaginary neobank Zuzu: - ✅ Modeled accounts for customers, assets, and revenue - ✅ Designed tran codes for bank transfers as well as ACH credits and debits - ✅ Organized customer accounts into a set and ran queries against it - ✅ Interrogated the balance history and entries for an account We hope that this has been useful for you to understand how Twisp works and what you can build with it. Obviously, this is an oversimplified example. Your product will certainly be more complex (and interesting)! > **Note:** > > We'd love to talk with you about your project. If you're interested, please [get in touch](https://www.twisp.com/?modal=Get+in+touch). --- # Ledger Invariants: Create and Attach Velocity Controls to Accounts In this tutorial we'll cover creating velocity controls in Twisp to ensure balances cannot exceed thresholds. Source: https://www.twisp.com/docs/tutorials/velocity ## How-to Guide: Add Velocity Controls in Twisp **Goal:** This guide shows you how to create and apply velocity controls to manage transaction limits on accounts or account sets in Twisp. **Prerequisites:** * Access to the Twisp GraphQL API. * Appropriate permissions to create/manage velocity controls and limits (`velocity.*` mutations). * UUIDs of the `Account` or `AccountSet` you wish to apply controls to. * Understanding of the specific limits you want to enforce (e.g., daily amount, transaction count). * (Optional) Familiarity with Common Expression Language (CEL) for defining complex `window`, `condition`, or `limit` expressions. ### Key Concepts * **Velocity Control:** A container that groups one or more `VelocityLimit`s. It defines an `enforcement` action (Warn, Void, Reject) and an optional `condition` (CEL expression) for when the control applies. * **Velocity Limit:** Defines a specific rule, such as a maximum amount or count within a defined `window`. It includes: * `name`/`description`: Human-readable identifiers. * `window`: CEL expressions defining the time frame or grouping criteria (e.g., daily, monthly, per merchant). Uses `PartitionKey`. * `limit`: The actual threshold (amount, layer, direction, start/end times). Uses `Limit`. * `currency`: The currency the limit applies to (or all if empty). * `condition`: Optional CEL expression for when this specific limit applies. * `params`: Optional parameters needed for dynamic limits (e.g., passing a specific merchant ID). * **Attachment:** Linking a `VelocityControl` to an `Account` or `AccountSet` makes the control active for that entity. Parameters required by the associated limits can be provided during attachment. ### Steps 1. **Define the Velocity Limit(s):** * Create each specific rule you need using the `createVelocityLimit` mutation. * Define its `name`, `description`, `window` (using `PartitionKeyInput`), `limit` (using `LimitInput`), `currency`, and optional `condition` and `params`. ```graphql # Example: Create a simple daily spending limit mutation CreateDailyLimit($limitId: UUID!) { createVelocityLimit(input: { velocityLimitId: $limitId name: "Daily Spending Limit" description: "Limit spending to $100 per day." currency: "USD" window: [{alias: "date", value: "context.vars.transaction.effective"}] # Daily window limit: { balance: [{ layer: "SETTLED" # Or PENDING, etc. amount: "decimal('100.00')" normalBalanceType: "DEBIT" # Limit debits }] } # Optional: condition: "context.vars.account.metadata.applyDailyLimit == true" # Optional: params: [{name: "maxAmount", type: DECIMAL}] # If limit amount was dynamic }) { velocityLimitId name } } ``` * *Variables:* `{ "limitId": "" }` 2. **Create the Velocity Control:** * Use the `createVelocityControl` mutation. * Define its `name`, `description`, `enforcement` action (e.g., `REJECT`), and optional overall `condition`. * Link the limits created in Step 1 using their `velocityLimitId`s in the `velocityLimitIds` array. ```graphql # Example: Create a control using the daily limit from Step 1 mutation CreateSpendingControl($controlId: UUID!, $limitId: UUID!) { createVelocityControl(input: { velocityControlId: $controlId name: "Standard Spending Control" description: "Enforces daily spending limits." enforcement: { action: REJECT } velocityLimitIds: [$limitId] # Link the limit(s) # Optional: condition: "!" + context.vars.account.metadata.exemptFromControl" }) { velocityControlId name limits { velocityLimitId } } } ``` * *Variables:* `{ "controlId": "", "limitId": "" }` 3. **Attach the Control to an Account or AccountSet:** * Use the `attachVelocityControl` mutation. * Provide the `velocityControlId` (from Step 2) and the target `accountId` (UUID of the `Account` or `AccountSet`). * If any attached limits defined `params`, provide their values in the `params` JSON object. ```graphql # Example: Attach the control to a specific account mutation AttachControlToAccount($controlId: UUID!, $accountId: UUID!) { attachVelocityControl( velocityControlId: $controlId accountId: $accountId # Optional: params: { "maxAmount": "150.00" } # If limit had params ) { velocityControlId name } } ``` * *Variables:* `{ "controlId": "", "accountId": "" }` ### Verification * Query the `VelocityControl` using its ID to confirm its limits. * Query the `Account` or `AccountSet` and check its `controls` or use the `attachedControls` query to see attached controls. * Post a transaction that should interact with the limit to ensure the expected enforcement action occurs. * Query the `velocity` field on the `Account` or `AccountSet` to check remaining limits. --- # Working with Journals In this tutorial, we will learn how to add additional journals to handle a multi-journal ledger system. Source: https://www.twisp.com/docs/tutorials/working-with-journals Journals are a fundamental tool in accounting, used to record transactions. In this tutorial, we will cover how to create a new journal, modify an existing one, query a journal, and lock a journal to prevent posting. Every ledger in Twisp starts with a **default journal**, but you can create as many additional journals as needed to suit your particular accounting structure. > **Task:** > > - Create a new journal using the `createJournal` mutation > - Update an existing journal using the `updateJournal` mutation > - Query a journal with the `journal` query > - Delete (lock) a journal using the `deleteJournal` mutation --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](/docs/tutorials/example-setup). ## Create a new journal You can create a new journal using the `createJournal` mutation. **Request** ```graphql mutation CreateJournal($journalGLId: UUID!) { createJournal( input: { journalId: $journalGLId name: "GL" description: "General Ledger" status: ACTIVE } ) { journalId name description status } } ``` **Response** ```json { "data": { "createJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "GL", "description": "General Ledger", "status": "ACTIVE" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` This will return the ID, name, description, and status of the newly created journal. Customize the input to suit your needs. ## Modify an existing journal To modify an existing journal, you can use the `updateJournal` mutation. Let's update the description of the newly created journal: **Request** ```graphql mutation UpdateJournal($journalGLId: UUID!) { updateJournal( id: $journalGLId input: { description: "_The_ ledger. The only one." } ) { journalId description history(first: 2) { nodes { version description } } } } ``` **Response** ```json { "data": { "updateJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "description": "_The_ ledger. The only one.", "history": { "nodes": [ { "version": 2, "description": "_The_ ledger. The only one." }, { "version": 1, "description": "General Ledger" } ] } } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` The response includes the UUID and the updated description of the journal, as well as the history of the changes made to the journal. ## Query a journal To query a journal, you can use the `journal` query and provide the ID to fetch. **Request** ```graphql query GetJournal($journalGLId: UUID!) { journal(id: $journalGLId) { name description status version } } ``` **Response** ```json { "data": { "journal": { "name": "GL", "description": "General Ledger", "status": "ACTIVE", "version": 1 } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` This will return the name, description, status, and version of the journal. ## Lock a journal to prevent posting Like other resources in Twisp, "deleting" a journal does not actually remove it from the database, but instead prevents it from being used by setting its status to `LOCKED`. To lock a journal to prevent posting, you can use the `deleteJournal` mutation. Let's lock our General Ledger journal: **Request** ```graphql mutation DeleteJournal($journalGLId: UUID!) { deleteJournal(id: $journalGLId) { journalId status } } ``` **Response** ```json { "data": { "deleteJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "status": "LOCKED" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## Conclusion Journals are a fundamental component of accounting, used to store collections of transactions. In this tutorial, we covered how to create a new journal, modify an existing one, query a journal, and lock a journal. These basic operations can be used to manage journals effectively and efficiently in your multi-journal accounting workflow.