# Branching Tenants and Managing Aliases Create stable tenant aliases, copy configuration into branch tenants, and move an alias to a new branch. Use a tenant alias when clients need a stable name, such as `twisp-dev`, while the tenant behind that name changes over time. Use a branch when you need a new, isolated tenant containing configuration copied from an existing tenant. Together, branching and aliases support a workflow like this: 1. Create an alias for the current development tenant. 2. Branch the tenant's configuration into a new tenant. 3. Validate the branch using its generated `accountId`. 4. Repoint the alias when the branch is ready. > **Note:** > > Aliases are unique per region. An alias can point only to a tenant in its owning organization, and only that organization can update or delete it. ## Before you begin Send these mutations to the GraphQL endpoint in the region containing the source tenant: ```text https://api..cloud.twisp.com/financial/v1/graphql ``` Authenticate the request and identify the source tenant with an explicit account ID: ```text authorization: Bearer content-type: application/json x-twisp-account-id: ``` Using a literal value in `x-twisp-account-id` for administrative requests keeps the source tenant explicit while an alias is being moved. For ordinary requests, clients can put a qualified alias in the same header, such as `x-twisp-account-id: alias/twisp-dev`. Alias fields in the GraphQL API use the raw alias name, such as `twisp-dev`. When an alias is supplied through `x-twisp-account-id`, qualify it as `alias/twisp-dev` so the server can distinguish it from a literal account ID. The `alias/` prefix is not stored and is not returned by alias queries or mutations. | Input | Value | Behavior | | --- | --- | --- | | GraphQL `alias` field | `twisp-dev` | Creates, updates, or identifies the raw alias name | | `x-twisp-account-id` | `twisp-dev` | Uses `twisp-dev` as a literal account ID | | `x-twisp-account-id` | `alias/twisp-dev` | Resolves the alias `twisp-dev` | The caller needs the following permissions: - Creating an alias requires `db:Insert` on `financial.aliases`. - Branching requires `db:Insert` on `financial.tenants` and permission to select and insert each configuration class being copied. ## 1. Create an alias Create the alias in the same region as its target tenant. The target `accountId` must belong to the caller's organization. Alias names and tenant account IDs follow the S3 bucket naming rules: - 3 to 63 characters. - Lowercase letters, numbers, periods (`.`), and hyphens (`-`) only. - Begins and ends with a letter or a number. - No two adjacent periods. - Not formatted as an IP address, such as `192.168.5.4`. A tenant account ID has one further restriction: it cannot begin with `alias`. This keeps an account ID from being read as the qualified form that `x-twisp-account-id` accepts. An alias name may begin with `alias`. **CreateAlias** ```graphql mutation CreateAlias($alias: String!, $accountId: String!, $ttlSeconds: Int = 60) { admin { createAlias( input: { alias: $alias accountId: $accountId ttlSeconds: $ttlSeconds } ) { alias accountId organizationId region ttlSeconds } } } ``` **Variables** ```json { "alias": "twisp-dev", "accountId": "4b3aef43-e770-4a51-aa8f-f73421f7045e", "ttlSeconds": 60 } ``` The mutation returns `twisp-dev` in its `alias` field. To address the tenant, qualify that name in the account ID header: ```shell curl 'https://api..cloud.twisp.com/financial/v1/graphql' \ -H 'authorization: Bearer ' \ -H 'content-type: application/json' \ -H 'x-twisp-account-id: alias/twisp-dev' \ --data-raw '{"query":"query CurrentOrganization { admin { organization { id name } } }"}' ``` ## 2. Point an alias at a different tenant Use `updateAlias` to move an existing alias to another tenant in the same organization. The alias name and owning organization do not change. **RepointAlias** ```graphql mutation RepointAlias($alias: String!, $accountId: String!, $ttlSeconds: Int = 60) { admin { updateAlias( input: { alias: $alias accountId: $accountId ttlSeconds: $ttlSeconds } ) { alias accountId region ttlSeconds } } } ``` **Variables** ```json { "alias": "twisp-dev", "accountId": "066dbb42-42d9-4605-a010-428b46c766c7", "ttlSeconds": 60 } ``` > **Warning:** > > Alias resolutions are cached. Updating `ttlSeconds` controls future cache entries; it does not invalidate an old mapping that a server has already cached. For a planned cutover, first lower the alias TTL, wait at least the previous TTL, and then repoint the alias. Raise the TTL again after the cutover is visible. ## 3. Branch a tenant `branch` generates a new tenant `accountId` and copies configuration from the tenant identified by the request header. The required `name` and `description` fields are stored on the new tenant and returned by tenant queries. Branch names cannot contain whitespace. **BranchTenant** ```graphql mutation BranchTenant { admin { branch(input: { name: "Development-branch" description: "Branched configuration for development" }) { id accountId organizationId name description } } } ``` Omitting `include` copies all supported configuration classes: - calculations - custom indexes - endpoints - non-default journals - tran codes - velocity controls and their configuration memberships - views and their backing table definitions - customer-defined clients Accounts, balances, entries, and transactions are not copied. Tenant creation supplies the new tenant's default journal and Twisp-managed clients. To copy only selected configuration, provide `include`: **BranchSelectedConfiguration** ```graphql mutation BranchSelectedConfiguration { admin { branch(input: { name: "Ledger-configuration-branch" description: "Branch containing selected ledger configuration" include: [Journal, TranCode, Calculation] }) { accountId } } } ``` The available values are `Calculation`, `CustomIndex`, `Endpoint`, `Journal`, `TranCode`, `VelocityControl`, and `View`. Include `View` when copying a custom index defined on a view. The branch operation is synchronous. Large configurations may take longer than a typical unary request deadline. ## 4. Branch and point an alias to the new branch Twisp's `@export` directive can capture the generated branch `accountId` and pass it to a later field in the same mutation. Top-level mutation fields execute serially, so keep the branch field before the alias update. **BranchAndRepointAlias** ```graphql mutation BranchAndRepointAlias($accountId: String = "") { branch: admin { branch(input: { name: "Development-branch" description: "Branched configuration for development" }) { accountId @export(as: "accountId") } } moveAlias: admin { updateAlias( input: { alias: "twisp-dev" accountId: $accountId ttlSeconds: 1 } ) { alias accountId region ttlSeconds } } } ``` The initial value of `$accountId` satisfies GraphQL variable validation. `@export` replaces it with the generated branch account ID before `moveAlias` executes. > **Warning:** > > Branch creation and alias updates commit through independent administrative transactions. If the branch succeeds but `updateAlias` fails, the branch remains and the alias continues pointing at its previous tenant. Use separate mutations when your workflow needs to retain the generated branch ID for explicit retry and recovery. For a planned low-downtime switch: 1. Lower the current alias TTL and wait out its previous TTL. 2. Branch the source tenant. 3. Test the new tenant using its explicit `accountId`. 4. Repoint the alias to the branch. 5. After the switch is visible, restore the normal alias TTL. --- # 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. ## 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](https://www.twisp.com/docs/accounting-core/chart-of-accounts.md) and [transaction codes](https://www.twisp.com/docs/tutorials/advanced/designing-tran-codes.md) 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](https://www.twisp.com/docs/reference/graphql/types/object.md#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](https://www.twisp.com/docs/tutorials/setting-up-accounts.md) and [Organizing With Account Sets](https://www.twisp.com/docs/tutorials/organizing-with-account-sets.md). ### 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](https://www.twisp.com/docs/tutorials/building-tran-codes.md) and [Designing Tran Codes](https://www.twisp.com/docs/tutorials/advanced/designing-tran-codes.md). ### 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](https://www.twisp.com/docs/infrastructure/security-and-auth.md). ## 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](https://www.twisp.com/docs/reference/graphql.md). 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](https://www.twisp.com/docs/reference/graphql/mutations.md#post-transaction), [query account balances](https://www.twisp.com/docs/reference/graphql/queries.md#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. 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](https://www.twisp.com/docs/accounting-core/balances.md) — 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](https://www.twisp.com/docs/accounting-core/balances.md) and [Pulling Balances](https://www.twisp.com/docs/tutorials/pulling-balances.md). --- # Handling ACH Returns Process and manage ACH returns effectively ## Coming Soon --- # Guides Use these how-to guides to build solutions for real-world financial systems using Twisp. --- # Modeling Banking on Twisp Design and Implement bank-like core accounting on Twisp. ## 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](https://www.twisp.com/docs/accounting-core/chart-of-accounts.md#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](https://www.twisp.com/docs/reference/graphql/types/object.md#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](https://www.twisp.com/docs/reference/graphql/types/object.md#account-set) to encapsulate the _total balance_ of the entity in question. 2. A default [Account](https://www.twisp.com/docs/reference/graphql/types/object.md#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](https://www.twisp.com/docs/accounting-core/encoded-transactions.md) define entries are posted for a particular transaction type. - [postTransaction](https://www.twisp.com/docs/reference/graphql/mutations.md#post-transaction) allows you to post a transaction with a specific tran code. - [voidTransaction](https://www.twisp.com/docs/reference/graphql/mutations.md#void-transaction) posts the entries required to reverse any transaction. - [workflows](https://www.twisp.com/docs/reference/graphql/mutations.md#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](https://www.twisp.com/docs/accounting-core/encoded-transactions.md) 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(type: PREPARED) { 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](https://www.twisp.com/docs/reference/graphql/mutations.md#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_CREATE_CR" }, { "amount": { "units": "1.00" }, "direction": "DEBIT", "entryType": "ACH_ENCUMBRANCE_CREATE_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](https://www.twisp.com/docs/reference/graphql/mutations.md#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. > **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](https://www.twisp.com/docs/reference/graphql/types/object.md#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](https://www.twisp.com/docs/accounting-core/chart-of-accounts.md#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](https://www.twisp.com/docs/reference/graphql/types/object.md#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 ## Coming Soon --- # Reconciling ACH Files Validate and reconcile ACH file processing ## Coming Soon