# Your First ACH Payment Send your first ACH credit payment In this tutorial, we will send our first ACH payment. We'll create a customer account, initiate a $50 credit payment, and generate an ACH file ready for transmission. ## What We'll Build By completing this tutorial, we will: - Create a customer account - Execute an ACH PUSH (credit) workflow - Generate an ACH file - Download the file for transmission - Verify the ledger entries This tutorial takes approximately 10 minutes to complete. ## Prerequisites Before we begin, you must have completed: - [Setting Up ACH Processing](https://www.twisp.com/docs/tutorials/ach/setting-up-ach.md) - This tutorial requires the IDs from setup You'll need these IDs from the setup tutorial: ``` Journal ID: 8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a Settlement: 37f7e8a6-171f-411d-ad59-7b1f40f505ea Fee: 5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d Config ID: fe27128a-b331-4e0e-94f8-9a32443fee36 ``` ## Step 1: Create a Customer Account First, we'll create an account for our customer who will receive the ACH payment. ```graphql mutation CreateCustomer { createAccount( input: { accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" code: "customer.alice.checking" name: "Alice Smith - Checking" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId name code } } ``` **What Just Happened:** We created a checking account for Alice Smith with ID `c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c`. This account represents Alice's bank account in our system. **Expected Response:** ```json { "data": { "createAccount": { "accountId": "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c", "name": "Alice Smith - Checking", "code": "customer.alice.checking" } } } ``` ## Step 2: Create the ACH Payment Now we'll create an ACH credit payment for $50. This uses the ACH PUSH workflow, which is for sending money to customers. ```graphql mutation CreatePayment { workflow { execute( input: { executionId: "55af980c-c1bb-11f0-833c-069b540ea27c" code: "ACH_PUSH" task: "CREATE" params: { accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" configId: "fe27128a-b331-4e0e-94f8-9a32443fee36" routingNumber: "026009593", accountNumber: "12345678901234567", accountType: "checking", individualName: "Alice Smith", entryDescription: "XFER", amount: "50.00" effective: "2025-11-15" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" feeAmount: "0.25" correlationId: "tutorial-payment-001" metadata: "{\"customer\": \"Alice Smith\", \"purpose\": \"Tutorial Payment\"}" entryMetadata: "{\"description\": \"Tutorial Payment\"}" } } ) { executionId task output { state } } } } ``` **What Just Happened:** We executed the CREATE task of the ACH PUSH workflow. This: - Created an pending transaction (hold) on Alice's account for $50 - Charged a $0.25 processing fee - Assigned execution ID for tracking **Key Parameters:** - `code: "ACH_PUSH"` - This is the ACH PUSH workflow UUID - `task: "CREATE"` - First state: create and encumber funds - `amount: "50.00"` - $50 payment - `effective: "2025-11-15"` - The date the payment should settle (use tomorrow's date in production) - `correlationId: "tutorial-payment-001"` - Unique identifier for tracking **Expected Response:** ```json { "data": { "workflow": { "execute": { "executionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "task": "CREATE", "output": { "state": "CREATE" } } } } } ``` **Save the Execution ID:** Copy the `executionId` from the response. We'll need it in the next step: ``` executionId: a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` ## Step 3: Check the Balance Let's verify the pending balance was created by checking Alice's account balance. ```graphql query CheckBalance { balance( accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" currency: "USD" ) { settled { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } pending { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } available (layer:PENDING) { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } normalBalance { formatted(as:{locale:"en-US"}) } } } } ``` **Expected Response:** ```json { "data": { "balance": { "settled": { "drBalance": { "formatted": "$0.25" }, "crBalance": { "formatted": "$0.00" } }, "pending": { "drBalance": { "formatted": "$50.00" }, "crBalance": { "formatted": "$0.00" } }, "available": { "drBalance": { "formatted": "$50.25" }, "crBalance": { "formatted": "$0.00" }, "normalBalance": { "formatted": "-$50.25" } } } } } ``` **What This Means:** - **Settled Layer**: $0.25 debit (the fee was charged immediately) - **Pending Layer**: $50 debit (funds are reserved, not yet settled) - **Available Balance**: -$50.25 (fees + pending amount) The negative available balance indicates Alice would need $50.25 in her account for this transaction to clear. ## Step 4: Generate the ACH File Now we'll generate an ACH file containing our payment. ```graphql mutation GenerateFile { ach { generateFile( input: { configId: "fe27128a-b331-4e0e-94f8-9a32443fee36" fileKey: "tutorial-payment-20251114.ach" fileType: ODFI_PUSH_ONLY generateEmpty: false } ) { fileKey generated } } } ``` Now we'll submit the payment for inclusion in an ACH file. This moves the payment from pending to settled and invokes the following workflow automatically: ```graphql mutation SubmitPayment { workflow { execute( input: { executionId: "55af980c-c1bb-11f0-833c-069b540ea27c" code: "ACH_PUSH" task: "SUBMIT" params: { effective: "2025-11-15" } } ) { executionId task output { state } } } } ``` **What Just Happened:** We requested generation of an ACH file that includes: - Reversed the pending - Posted to the settled layer - All submitted PUSH (credit) transactions - NACHA-formatted, ready for transmission - Stored with key `tutorial-payment-20251114.ach` **Expected Response:** ```json { "data": { "ach": { "generateFile": { "fileKey": "tutorial-payment-20251114.ach", "generated": true } } } } ``` **If `generated: false`:** This means no transactions were ready for file generation. ## Step 5: Verify the Settled Balance Let's check the balance again to see the settled transaction. ```graphql query CheckBalance { balance( accountId: "c4f7e3a2-8b1d-4e9f-a5c6-2d3e4f5a6b7c" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" currency: "USD" ) { settled { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } pending { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } } available (layer:PENDING) { drBalance { formatted(as:{locale:"en-US"}) } crBalance { formatted(as:{locale:"en-US"}) } normalBalance { formatted(as:{locale:"en-US"}) } } } } ``` **Expected Response:** ```json { "data": { "balance": { "settled": { "drBalance": { "formatted": "$50.25" }, "crBalance": { "formatted": "$0.00" } }, "pending": { "drBalance": { "formatted": "$0.00" }, "crBalance": { "formatted": "$0.00" } }, "available": { "drBalance": { "formatted": "$50.25" }, "crBalance": { "formatted": "$0.00" }, "normalBalance": { "formatted": "-$50.25" } } } } } ``` **What Changed:** - **Settled Layer**: Now $50.25 debit ($50 payment + $0.25 fee) - **Pending Layer**: Cleared to $0 (funds moved to settled) - **Available Balance**: $50.25 debit (the finalized transaction amount) The payment is now finalized and in a file on the way to the Federal Reserve! ## Step 6: Download the ACH File Now we'll get a download URL for our generated file. ```graphql mutation GetDownloadURL { files { createDownload( key: "tutorial-payment-20251114.ach" ) { downloadURL downloadURLExpiration } } } ``` **Expected Response:** ```json { "data": { "files": { "createDownload": { "downloadURL": "https://s3.amazonaws.com/bucket/path?signature=...", "downloadURLExpiration": "2025-11-14T15:30:00Z" } } } } ``` **Download the File:** Use the URL to download the file (replace `` with actual URL from response): ```bash curl '' -o tutorial-payment-20251114.ach ``` ## Step 7: Inspect the ACH File Let's look at what's in the file we generated. Open `tutorial-payment-20251114.ach` in a text editor. You'll see a NACHA-formatted file with lines like: ``` 101 021000021 1234567892511150055A094101Test Bank Your Company 5220Your Company 1234567890PPDXFER 251114251117 1123456780000001 622026009593123456789012345670000005000 Alice Smith 0123456780000001 822000000100026009590000000000000000000050001234567890 123456780000001 9000001000001000000010002600959000000000000000000005000 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999 ``` **What Each Line Means:** - **Line 1 (101...)**: File Header - Contains ODFI info - **Line 2 (5225...)**: Batch Header - Contains company info - **Line 3 (622...)**: Entry Detail - The actual $50 payment to Alice - **Line 4 (822...)**: Batch Control - Validates batch totals - **Line 5 (9000...)**: File Control - Validates file totals The file is ready to transmit to your ODFI! ## Step 9: View the Workflow Execution Let's review the complete workflow execution to see all the ledger entries. ```graphql query ViewExecution { workflow { execution( executionId: "55af980c-c1bb-11f0-833c-069b540ea27c" ) { executionId workflowId task params activities { action entityType entity { ... on Transaction { transactionId effective entries(first:10) { nodes { entryId accountId layer direction units } } } } } } } } ``` **What You'll See:** This shows all ledger transactions created by the workflow: - CREATE task transactions (pending + fee) - SUBMIT task transactions (reverse pending + settle) Each activity shows the double-entry accounting entries on customer and settlement accounts. ## What We Accomplished Let's review what we did: 1. ✅ Created a customer account 2. ✅ Created a $50 ACH credit payment 3. ✅ Verified the pending transaction was created 4. ✅ Generated an ACH file 5. ✅ Verified the payment settled 6. ✅ Downloaded the ACH file 7. ✅ Inspected the NACHA file format 8. ✅ Reviewed the workflow execution ## Key Concepts We Learned **Workflow States:** - CREATE: Pending funds, charge fees - SUBMIT: Settle funds, mark for file generation **Balance Layers:** - Pending: Temporary holds - Settled: Finalized transactions - Available: Calculated from settled - pending **File Generation:** - Collects all submitted transactions - Generates NACHA-formatted files - Ready for ODFI transmission ## What Happens Next? In production, you would: 1. Transmit the generated file to your ODFI via SFTP 2. Wait for confirmation (usually 1-2 business days) 3. Process any returns received from the RDFI For this tutorial, you now have: - A working ACH payment workflow - A valid NACHA file - Understanding of the complete process ## Try It Again Now that you understand the process, try creating another payment: 1. Create a new customer account (use a different UUID) 2. Execute CREATE with a different amount 3. Submit the payment 4. Generate a new file Each time you generate a file, all submitted payments will be included. ## Troubleshooting **Problem: "Execution not found"** Make sure you're using the correct `executionId` from the CREATE response in the SUBMIT mutation. **Problem: "File generated: false"** This means no transactions were ready. Check: - Did you run the CREATE task? - Is the effective date in the future or today? - Are there any errors in the workflow execution? **Problem: "Cannot download file"** The download URL expires after a short time. If it's expired: - Run the `createDownload` mutation again to get a new URL **Problem: "Balance doesn't match"** After CREATE: - Pending should show $50 DR - Settled should show $0.25 DR (fee) After SUBMIT: - Pending should be $0 - Settled should show $50.25 DR ## Next Steps Now that you've sent your first ACH payment, explore more: **Processing Guides:** - [Processing ACH Payments](https://www.twisp.com/docs/guides/processing-ach-payments.md) - Production payment workflows - [Handling ACH Returns](https://www.twisp.com/docs/guides/handling-ach-returns.md) - Managing payment returns - [Reconciling ACH Files](https://www.twisp.com/docs/guides/reconciling-ach-files.md) - File validation and reconciliation **Reference Documentation:** - [ODFI Reference](https://www.twisp.com/docs/reference/ach/odfi.md) - Complete ODFI API documentation - [File Operations](https://www.twisp.com/docs/reference/ach/file-operations.md) - File upload and download operations - [Configuration](https://www.twisp.com/docs/reference/ach/configuration.md) - ACH configuration reference ## Summary Congratulations! You've successfully: - Created your first ACH payment - Executed a complete workflow from creation to settlement - Generated a NACHA-formatted ACH file - Understood the balance layer transitions - Downloaded a production-ready ACH file You now have the foundation to build production ACH payment systems on Twisp! --- # Setting Up ACH Processing Learn how to set up ACH processing from scratch In this tutorial, we will set up ACH processing on the Twisp platform. By the end, you will have a fully configured ACH processor ready to send and receive ACH transactions. ## What We'll Build By completing this tutorial, we will: - Create all required accounts for ACH processing - Set up a journal for ACH transactions - Configure a webhook endpoint for transaction decisioning - Create an ACH configuration - Verify everything works correctly This tutorial takes approximately 15 minutes to complete. ## Prerequisites Before we begin, you need: - A Twisp account with API access - Your GraphQL API endpoint URL - API credentials (authentication token) You do NOT need: - An ODFI relationship (that comes later for production) - Understanding of double-entry accounting - Prior ACH experience ## Step 1: Create the Journal We will start by creating a journal where all ACH transactions will be posted. ```graphql mutation CreateJournal { createJournal( input: { journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" name: "ACH Processing Journal" status: ACTIVE } ) { journalId name status } } ``` **What Just Happened:** We created a journal with ID `8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a`. This journal will track all ACH-related accounting entries. **Expected Response:** ```json { "data": { "createJournal": { "journalId": "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a", "name": "ACH Processing Journal", "status": "ACTIVE" } } } ``` ## Step 2: Create Required Accounts Next, we will create four accounts required for ACH processing. We'll create them all at once. ```graphql mutation CreateACHAccounts { # Settlement account - where funds transit settlement: createAccount( input: { accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" code: "settlement.ach" name: "ACH Settlement" normalBalanceType: DEBIT config: { enableConcurrentPosting: true } } ) { accountId name } # Suspense account - for transactions to unknown accounts suspense: createAccount( input: { accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" code: "suspense.ach" name: "ACH Suspense" config: { enableConcurrentPosting: true } } ) { accountId name } # Exception account - for failed transactions exception: createAccount( input: { accountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" code: "exception.ach" name: "ACH Exception" config: { enableConcurrentPosting: true } } ) { accountId name } # Fee account - for ACH processing fees fee: createAccount( input: { accountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" code: "fee.ach" name: "ACH Fee Income" normalBalanceType: CREDIT config: { enableConcurrentPosting: true } } ) { accountId name } } ``` **What Just Happened:** We created four accounts: 1. **Settlement Account** (`37f7e8a6-171f-411d-ad59-7b1f40f505ea`) - All ACH funds flow through this account during processing 2. **Suspense Account** (`3171b0c2-6e9f-41aa-a5a6-ee927deb27cf`) - Holds transactions when the destination account can't be found 3. **Exception Account** (`4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c`) - Receives transactions that fail processing rules 4. **Fee Account** (`5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d`) - Collects ACH processing fees All accounts have `enableConcurrentPosting: true` to support high transaction volumes. **Expected Response:** ```json { "data": { "settlement": { "accountId": "37f7e8a6-171f-411d-ad59-7b1f40f505ea", "name": "ACH Settlement" }, "suspense": { "accountId": "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf", "name": "ACH Suspense" }, "exception": { "accountId": "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c", "name": "ACH Exception" }, "fee": { "accountId": "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d", "name": "ACH Fee Income" } } } ``` ## Step 3: Create the Webhook Endpoint Now we will create a webhook endpoint. This endpoint will receive requests for transaction decisioning when ACH files are processed. For this tutorial, we'll use a test endpoint URL. In production, you'll replace this with your actual webhook server. ```graphql mutation CreateWebhook { events { createEndpoint( input: { endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" status: ENABLED endpointType: ACH_PROCESSOR url: "https://webhook.site/unique-url-here" subscription: [] description: "ACH decisioning webhook" } ) { endpointId url status } } } ``` **What Just Happened:** We created a webhook endpoint with ID `b84512f1-a67e-4dc2-94dd-66c48b4d13fb`. When ACH files are processed, Twisp will send POST requests to the URL we specified. **For This Tutorial:** Use `https://webhook.site` to create a free test webhook URL: 1. Go to https://webhook.site 2. Copy the unique URL shown 3. Use that URL in the mutation above **Expected Response:** ```json { "data": { "events": { "createEndpoint": { "endpointId": "b84512f1-a67e-4dc2-94dd-66c48b4d13fb", "url": "https://webhook.site/your-unique-url", "status": "ENABLED" } } } } ``` ## Step 4: Create the ACH Configuration Now we will tie everything together by creating an ACH configuration. This tells the ACH processor which accounts to use and where to send webhooks. ```graphql mutation CreateACHConfig { ach { createConfiguration( input: { configId: "fe27128a-b331-4e0e-94f8-9a32443fee36" endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb" journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a" settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea" suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf" exceptionAccountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c" feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d" odfiHeaderConfiguration: { immediateDestination: "021000021" immediateDestinationName: "Test Bank" immediateOrigin: "1234567890" immediateOriginName: "Your Company" } timeZone: "America/New_York" } ) { configId version timeZone } } } ``` **What Just Happened:** We created an ACH configuration that connects: - The journal we created - All four accounts - The webhook endpoint - ODFI header information for generating ACH files The `odfiHeaderConfiguration` contains placeholder values. When you're ready for production, you'll replace these with real values from your ODFI (bank). **Expected Response:** ```json { "data": { "ach": { "createConfiguration": { "configId": "fe27128a-b331-4e0e-94f8-9a32443fee36", "version": 1, "timeZone": "America/New_York" } } } } ``` ## Step 5: Verify the Configuration Let's verify everything was created correctly by querying our configuration. ```graphql query VerifySetup { ach { configuration( id: "fe27128a-b331-4e0e-94f8-9a32443fee36" ) { configId journalId settlementAccountId suspenseAccountId exceptionAccountId feeAccountId endpointId odfiHeaderConfiguration { immediateDestination immediateOrigin } timeZone version } } } ``` **Expected Response:** ```json { "data": { "ach": { "configuration": { "configId": "fe27128a-b331-4e0e-94f8-9a32443fee36", "journalId": "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a", "settlementAccountId": "37f7e8a6-171f-411d-ad59-7b1f40f505ea", "suspenseAccountId": "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf", "exceptionAccountId": "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c", "feeAccountId": "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d", "endpointId": "b84512f1-a67e-4dc2-94dd-66c48b4d13fb", "odfiHeaderConfiguration": { "immediateDestination": "021000021", "immediateOrigin": "1234567890" }, "timeZone": "America/New_York", "version": 1 } } } } ``` **What to Check:** - All IDs match what we created - Version is 1 (this is the first version) - Timezone is correct for your location If everything matches, congratulations! Your ACH processor is configured. ## What We Accomplished Let's review what we built: 1. ✅ Created a journal for ACH transactions 2. ✅ Created four required accounts (settlement, suspense, exception, fee) 3. ✅ Set up a webhook endpoint 4. ✅ Created an ACH configuration connecting everything 5. ✅ Verified the configuration is correct ## Save These IDs You'll need these IDs going forward. Save them somewhere safe: ``` Journal ID: 8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a Settlement: 37f7e8a6-171f-411d-ad59-7b1f40f505ea Suspense: 3171b0c2-6e9f-41aa-a5a6-ee927deb27cf Exception: 4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c Fee: 5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d Webhook: b84512f1-a67e-4dc2-94dd-66c48b4d13fb Config ID: fe27128a-b331-4e0e-94f8-9a32443fee36 ``` ## Next Steps Now that your ACH processor is set up, you're ready to send your first ACH payment! **Continue to the next tutorial:** - [First ACH Payment](https://www.twisp.com/docs/tutorials/ach/first-ach-payment.md) - Send a $50 test payment and generate an ACH file **Production Checklist:** Before using ACH in production, you'll need to: - [ ] Establish a relationship with an ODFI (bank) - [ ] Get real ODFI header values from your bank - [ ] Update the `odfiHeaderConfiguration` with real values - [ ] Set up SFTP/FTPS for file transmission - [ ] Implement a production webhook server - [ ] Test with your ODFI's validation process ## Troubleshooting **Problem: "Account already exists" error** If you see this error, it means you already have an account with that ID. This is fine! You can either: - Use the existing accounts and skip account creation - Choose different UUIDs for your accounts **Problem: "Webhook endpoint creation failed"** Common causes: - Invalid URL format - make sure it starts with `https://` - Network issue - try again in a moment **Problem: "Configuration creation failed"** Check that: - All referenced IDs (journal, accounts, endpoint) exist - You haven't already created a configuration with this ID - All UUIDs are properly formatted ## Summary You've successfully set up ACH processing! We created: - A journal for transaction tracking - Four accounts for different processing scenarios - A webhook endpoint for transaction decisioning - An ACH configuration tying everything together You're now ready to process ACH transactions. The next tutorial will show you how to send your first payment. --- # Core Admin: Managing Tenants, Users, and Groups In this tutorial, we'll cover the administrative tasks involved in a Twisp ledger: managing tenants, users, and groups. ## Introduction to Tenants, Users, and Groups In any organization, especially in the context of accounting and financial systems, managing access control and permissions is crucial to ensure data security and prevent unauthorized access. Twisp provides a robust system to manage access control through [Tenants](https://www.twisp.com/docs/reference/graphql/types/object.md#tenant), [Users](https://www.twisp.com/docs/reference/graphql/types/object.md#user), and [Groups](https://www.twisp.com/docs/reference/graphql/types/object.md#group). ### Tenants A [Tenant](https://www.twisp.com/docs/reference/graphql/types/object.md#tenant) in Twisp represents an environment within an organization, typically associated with a specific application, service, or set of resources. Tenants contain isolated ledgers, each deployed to a specific region. They play a vital role in isolating data and configurations between different environments. Each tenant is uniquely identified by an `accountId`, which in combination with an AWS region, is used to calculate the database tenant for data isolation purposes. By isolating the ledgers and associated resources, Tenants ensure that each environment remains independent, preventing accidental or unauthorized access to sensitive data from other environments. ### Users [Users](https://www.twisp.com/docs/reference/graphql/types/object.md#user) are human members within an organization who interact with the Twisp accounting core. Each user is uniquely identified by their email address. Users can belong to multiple [Groups](https://www.twisp.com/docs/reference/graphql/types/object.md#group), which define their permissions within the organization based on the associated policies of each group. The effective permissions of a user are determined by the combined set of policies from all their groups. ### Groups [Groups](https://www.twisp.com/docs/reference/graphql/types/object.md#group) are a logical grouping of users within an organization. They are used to manage access control and permissions for users. Each Group can have one or more associated policies that define the allowed actions for its member users. Users can belong to multiple Groups, and their permissions are determined by the combined set of policies from all their groups. ## Working with Users and Groups ### Managing Users To create a new User, you can use the `admin.createUser` mutation, providing the necessary input such as the user's unique ID (UUID), email address, and the group IDs the user should belong to. **Request** ```graphql mutation AdminCreateUser { admin { createUser( input: { id: "9cc8bd28-a36d-502e-89fd-7f1410c1b90a" groupIds: ["d57bd759-73d5-4452-a73e-12b590324e35"] email: "george@twisp.com" } ) { id email groupIds } } } ``` **Response** ```json { "data": { "admin": { "createUser": { "id": "9cc8bd28-a36d-502e-89fd-7f1410c1b90a", "email": "george@twisp.com", "groupIds": ["d57bd759-73d5-4452-a73e-12b590324e35"] } } } } ``` > **Note:** > > Newly created users will automatically receive an invitation email with a link to the Twisp console. To update an existing User's details, such as their email address or group memberships, you can use the `admin.updateUser` mutation with the corresponding input. ### Managing Groups To create a new Group, use the `admin.createGroup` mutation, providing the necessary input such as the group's unique ID (UUID), name, description, and policy. **Request** ```graphql mutation AdminCreateGroup { admin { createGroup( input: { id: "917e123a-b89d-4ab5-b11c-cdf6aac80b63" name: "empty-policy-1" description: "A group with an empty policy" policy: "[]" } ) { name description policy } } } ``` **Response** ```json { "data": { "admin": { "createGroup": { "name": "empty-policy-1", "description": "A group with an empty policy", "policy": "[]" } } } } ``` To update an existing Group's details, such as the name, description, or policy, use the `admin.updateGroup` mutation with the appropriate input. **Request** ```graphql mutation AdminUpdateGroup { admin { updateGroup( input: { name: "empty-policy-1" description: "An empty policy layer will default to the base policy." policy: "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"], \"assertions\": {\"always false\": \"1 == 0\"}}]" } ) { name description policy } } } ``` **Response** ```json { "data": { "admin": { "updateGroup": { "name": "empty-policy-1", "description": "An empty policy layer will default to the base policy.", "policy": "[{\"actions\": [\"*\"],\"effect\": \"DENY\",\"resources\":[\"*\"], \"assertions\": {\"always false\": \"1 == 0\"}}]" } } } } ``` ### Deleting Users and Groups To delete an existing user or group, use the `admin.deleteUser` or `admin.deleteGroup` mutation. You will need to provide the email address of the user you want to delete, or the name of the group. For example, this mutation deletes the user with the specified email address and returns the deleted user's email: **Request** ```graphql mutation AdminDeleteUser { admin { deleteUser(email: "george@twisp.com") { email } } } ``` **Response** ```json { "data": { "admin": { "deleteUser": { "email": "george@twisp.com" } } } } ``` ## Creating a Tenant A Tenant represents an environment within an organization, used for isolating data and configurations for specific applications or services. By creating a Tenant, you can ensure that your different environments have separate ledgers and resources, improving organization and security. ### Using the `admin.createTenant` mutation To create a new [Tenant](https://www.twisp.com/docs/reference/graphql/types/object.md#tenant), you will need to use the `admin.createTenant` mutation, providing the necessary details such as `id`, `accountId`, `name`, and `description`. The `accountId` is especially important, as it is used in combination with an AWS region to isolate the tenant's data. Here's an example of a GraphQL mutation to create a new tenant: **Request** ```graphql mutation AdminCreateTenant { admin { createTenant( input: { id: "72a0097f-239e-48ec-a417-49c318332ed6" accountId: "sandbox" name: "Sandbox" description: "Sandbox tenant for testing" ephemeral: true } ) { accountId name created ephemeral } } } ``` **Response** ```json { "data": { "admin": { "createTenant": { "accountId": "sandbox", "name": "Sandbox", "created": "2000-01-01T00:00:00Z", "ephemeral": true } } } } ``` ## Assigning Permissions and Access Control Permissions are determined by the policies associated with each [Group](https://www.twisp.com/docs/reference/graphql/types/object.md#group) a [User](https://www.twisp.com/docs/reference/graphql/types/object.md#user) belongs to. The effective permissions for a user are calculated based on the combined set of policies from all their groups. For example, if a user belongs to two groups with different policies, their effective permissions will be the result of evaluating both policies together. To assign groups and permissions to users, you can use the `admin.updateUser` mutation by providing the user's ID and the desired group IDs. This mutation allows you to update the user's group membership, determining their effective permissions within the organization. Here's an example: **Request** ```graphql mutation AdminUpdateUser { admin { updateUser( input: { email: "george@twisp.com" groupIds: ["152f0c89-6cba-53c9-955e-16d7cbc1f35e"] } ) { email groupIds } } } ``` **Response** ```json { "data": { "admin": { "updateUser": { "email": "george@twisp.com", "groupIds": ["152f0c89-6cba-53c9-955e-16d7cbc1f35e"] } } } } ``` In this example, the user's group is changed because a new group ID is provided. ### Verifying Effective Permissions for Users To ensure that users have the correct permissions, it is essential to verify their effective permissions, which are determined by the combined set of policies from all their associated groups. To check a user's effective permissions, review the policies associated with each group the user belongs to, taking note of any `ALLOW` or `DENY` effects on actions and resources. The user must have at least one `ALLOW` policy for each desired action and resource but will be blocked by any `DENY` policy on the same action and resource. ### Monitoring and Adjusting Access as Needed It is crucial to regularly monitor and adjust user access to maintain a secure environment. You can use the `admin` queries to retrieve details about users, groups, and their permissions within the organization. For example, to get a list of users and their associated groups, use the following query: **Request** ```graphql query GetAdminUsers { admin { users(first: 5) { nodes { email organizationId groupIds } } } } ``` **Response** ```json { "data": { "admin": { "users": { "nodes": [ { "email": "test@twisp.com", "organizationId": "932343ab-33ee-410d-9663-7ddaec4d5bfa", "groupIds": ["4dcef8d5-186b-4db1-aa00-8d5b2eee85f8"] } ] } } } } ``` ## Conclusion In this tutorial, we have explored the concepts and functionalities of [Tenants](https://www.twisp.com/docs/reference/graphql/types/object.md#tenant), [Users](https://www.twisp.com/docs/reference/graphql/types/object.md#user), and [Groups](https://www.twisp.com/docs/reference/graphql/types/object.md#group) within the Twisp Accounting Core. We have learned the importance of managing access control and permissions to maintain a secure and well-organized environment. By following the steps outlined in this tutorial, you should now have a solid understanding of how to manage Tenants, Users, and Groups effectively within Twisp. --- # Designing a Tran Code In this tutorial, we will cover some of the factors that go into designing a transaction code. ## Preliminary Questions Tran codes should mirror a type of transaction that your system handles. They are meant to encapsulate and abstract accounting activity, and so the first step to designing a good tran code is **having a clear understanding of what that activity is**. Thus, before we get into designing a tran codes, it is important to first clarify: - What **type** of transaction is this? ACH transfer? Credit card purchase? Foreign exchange? Something else? - Which **accounts** are involved? - Where is the money coming from and where is it going? - Are there any additional accounts that need to be debited/credited? - How many **entries** should be written to the ledger? - Which entries go on the debit side and which on the credit side? - Do these entries reflect a **settled** amount, or are they still **pending** a final settlement? ## Naming and Documenting The **type** of transaction should be used to give the tran code a good name through its `code` field. The `code` field is the primary human-friendly identifier. It should provide a concise indication of what the tran code does and is used for. We recommend using codes that just give enough information to be easily identifiable without being over-wordy. For consistency, we recommend using UPPER_SNAKE_CASE formatting for the `code`. Prefer: - ✅ `ACH_CREDIT` - ✅ `CARD_HOLD_CANCEL` - ✅ `INTEREST_ADJUSTMENT` Avoid: - ❌ `ACH` - ❌ `CancelHold` - ❌ `adjusting_interest_for_personal_loan_accounts` > **Note:** > > Depending on the size and complexity of your tran code library, you may choose to implement more formalized patterns and structures for naming your tran codes. For example, some organizations might use abbreviated versions of operations (`HLD`, `STL`, `DEP`, `CLR`) to keep tran code names extra terse. Of course, only so much information can be communicated through a short string of characters. Because tran codes act as the API for your funds flow, they should also be well documented. The `description` field is where this documentation can live. Use it to add additional context about why the tran code exists, how it should be used, and whatever other information would benefit those interacting with your ledger. This field supports Markdown formatting. ## Defining the Transaction & Ledger Entries Posted transactions and the ledger entries written are defined within the `transaction` and `entries` fields, respectively. These are effectively templates used to generate the [Transaction](https://www.twisp.com/docs/reference/graphql/types/object.md#transaction) and [Entries](https://www.twisp.com/docs/reference/graphql/types/object.md#entry) records. Within the `transaction` field, we can define values that describe important aspects of the transaction like its `effective` date and which `journal` it should be written to. Other [Transaction](https://www.twisp.com/docs/reference/graphql/types/object.md#transaction) fields like the `correlationId` and `metadata` can also be defined here, although they are optional. The `entries` field is a list of the templates for the [Entries](https://www.twisp.com/docs/reference/graphql/types/object.md#entry) written to the ledger. Each ledger entry must define its `accountId`, amount (in `units` and `currency`), `direction` (DEBIT or CREDIT), `layer` (SETTLED, PENDING, or ENCUMBRANCE), and `entryType`. Optionally, a `description` may be written here as well. > **Note:** > > The `entryType` for an entry is similar to the `code` for a tran code: it is a short identifier for describing the type of activity that the entry represents. In many cases, the `entryType` is just an extension of the `code`. For example, an `ACH_CREDIT_FEE` tran code might write ledger entries with types `ACH_CREDIT_FEE_DR` for the debit-side and `ACH_CREDIT_FEE_CR` for the credit-side entry. How these entry definitions are written depends upon the transaction type and other information gathered as part of the pre-design process. **Request** ```graphql mutation ACHCreditTC( $achCreditId: UUID! $journalId: Expression! $achSettlementAcctId: Expression! $exampleUserAcctId: Expression! ) { achCredit: createTranCode( input: { tranCodeId: $achCreditId code: "ACH_CREDIT" description: "An ACH credit into an account." transaction: { journalId: $journalId, effective: "date('2000-01-01')" } entries: [ { accountId: $achSettlementAcctId units: "decimal('11.25')" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: $exampleUserAcctId units: "decimal('11.25')" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "achCredit": { "tranCodeId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b" } } } ``` **Variables** ```json { "achCreditId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b", "journalId": "uuid('8345d4a6-e100-4f70-9a31-d458acb3553e')", "achSettlementAcctId": "uuid('38897a35-0de5-4055-9a87-dc5256fc96b7')", "exampleUserAcctId": "uuid('fcb5a92f-cafb-4076-93dd-5df6d759a482')" } ``` ## Parameterizing Inputs In most cases, not all of the information needed to write transactions and entries is available at the time of designing a tran code, but instead needs to be passed in at runtime (i.e. when the transaction is posted). For example, most transactions are not for fixed amounts, and so these amounts need to be specified when posting the transaction. This is where the `params` field of a tran code comes in. With the `params`, we can define parameters of a transaction which can then be referenced inside of the values defined for the `transaction` and `entries`. Say we wanted to write entries where an `amount` (in decimal units) is supplied at posting time. To do this, we need to do two things: 1. Define an `amount` parameter inside of the `params` object. 2. Reference the `amount` value from `params` within the `units` field of our entries. A modification to the above tran code definition shows how this parameterization would work: **Request** ```graphql mutation ACHCreditTC( $achCreditId: UUID! $journalId: Expression! $achSettlementAcctId: Expression! ) { achCredit: createTranCode( input: { tranCodeId: $achCreditId code: "ACH_CREDIT" description: "An ACH credit into an account." params: [ { name: "account", type: UUID, description: "Deposit account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "effective" type: DATE description: "Effective date for ACH transaction." } { name: "currency" type: STRING description: "Currency code for entries. Defaults to 'USD'." default: "USD" } ] transaction: { journalId: $journalId, effective: "params.effective" } entries: [ { accountId: $achSettlementAcctId units: "params.amount" currency: "params.currency" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "params.currency" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "achCredit": { "tranCodeId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b" } } } ``` **Variables** ```json { "achCreditId": "e77bf1d8-2e44-4905-b6bd-25fe239af03b", "journalId": "uuid('8345d4a6-e100-4f70-9a31-d458acb3553e')", "achSettlementAcctId": "uuid('38897a35-0de5-4055-9a87-dc5256fc96b7')" } ``` Note the change to `params.amount` inside of the `units` fields. Because these fields accept [CEL expressions](https://www.twisp.com/docs/reference/cel.md), we can reference fields on the runtime `params` object to access the values passed in. You can see the use of `params` in action: [Tran Code Invocation](https://www.twisp.com/docs/reference/ledger/tran-codes.md#tran-code-invocation). In this way, the tran code can accept any number of parameterized values at runtime an inject them into the [Transaction](https://www.twisp.com/docs/reference/graphql/types/object.md#transaction) and [Entries](https://www.twisp.com/docs/reference/graphql/types/object.md#entry) written. --- # Exporting Data with Warehouse Learn how to export your Twisp ledger data using the warehouse export API. Twisp’s **Warehouse Export** feature allows you to export ledger data into files you can download using the [Files API](https://www.twisp.com/docs/reference/graphql/mutations.md#files-"files"). This feature is available to all tenants, even if you do not have a managed data warehouse configured for your environment. This tutorial will guide you through using the export API, explain key parameters, and share best practices for effective and efficient data exports. ## Why Use Warehouse Export? - **Incremental & Historical Exports:** Export only new/changed records, or retrieve all historical versions. - **Chunked, Compressed Exports:** Data is written in compressed files suitable for large-scale download and ingestion. - **Supports All Ledger Data:** Export accounts, balances, entries, transactions, and more, in bulk. ## Example To initiate an export for all balances for a specific date, use the following GraphQL mutation: ```graphql mutation Export { warehouse { export( input: { entity: Balance version: LATEST format: JSON compression: GZIP destination: { files: { keyPrefix: "exports/2025/07/02/balance/" } } fromTimestamp: "2025-07-01T00:00:00.000Z" toTimestamp: "2025-07-02T00:00:00.000Z" } ) { id } } } ``` This starts an export job and returns an `id` you can use to track its progress. To check the status and list files created, use: ```graphql query ExportStatus { warehouse { export(id: "YOUR_EXPORT_ID") { id status # (e.g., RUNNING, FINISHED, FAILED) error } } files { list(keyPrefix: "exports/2025/07/02/balance/") } } ``` When finished, the files (e.g., `balance/000.json.gz`, `balance/001.json.gz`, etc.) are available for download through the Files API. --- ## Key Parameters | Parameter | Description | |--------------------|--------------------------------------------------------------------------------------------------| | `entity` | The type of data to export (`Balance`, `Entry`, `Account`, etc.). | | `version` | `LATEST` exports only the most recent version per record; `HISTORY` exports all record versions. | | `format` | Format for the file (`JSON` is recommended for most usage). | | `compression` | Output compression (`GZIP` recommended for efficient transfer). | | `destination` | Where exported files are stored in Twisp Files (e.g., `{ files: { keyPrefix: "my/path" } }`). | | `fromTimestamp` | (Optional) Start export with records created/modified after this timestamp (inclusive). | | `toTimestamp` | (Optional) End export with records before this timestamp (exclusive). | > **Note:** > > **Tip:** Use `fromTimestamp` and `toTimestamp` for incremental exports—ideal for extracting only the new or modified data since your last export. This enables efficient change data capture (CDC) workflows for data lakes and reporting pipelines. ## Understanding LATEST vs HISTORY - **`version: LATEST`** Exports just the most current version of every record in the chosen entity. Use this for up-to-date snapshots of your ledger (e.g., current balances, current state of accounts). - **`version: HISTORY`** Exports all versions for every record. This is useful for: - Auditing - Reconstructing point-in-time states (e.g., balance history, account history) - Regulatory compliance > **Note:** > > To pull **point-in-time balances or entries** for reporting, use `version: HISTORY` and filter using `fromTimestamp` and `toTimestamp` to get just the versions active during a desired period. ## Output Schema Details - **File Schema:** The exported data files follow the [Warehouse entity schemas](https://www.twisp.com/docs/reference/ledger/warehouse.md#views-and-schemas) (e.g., `balance`, `entry`), so you can use the same field definitions. - **Amounts:** For types like `balance` and `entry`, number fields (such as monetary amounts) will include a stand-alone `_units` column representing the value as a number for convenience. ## Downloading Exported Files Once the export job has completed: 1. Use the `files.list` API to enumerate all files at your chosen prefix. 2. Download each file with the `files.createDownload` mutation to receive a presigned URL. ## Additional Tips - **Large Exports:** For very large exports, the system will automatically shard data into multiple files (e.g., `balance/000.json.gz`, `balance/001.json.gz`, etc.). - **Performance:** Using compressed (`GZIP`) export is highly recommended for both speed and cost savings. --- # Multi-Journal Accounting In this tutorial, we will explore how to perform multiple-journal accounting using the Twisp Accounting Core. Our primary focus will be on setting up a multi-journal ledger and posting various transactions across these journals to demonstrate how balances are materialized in accounts and account sets across journals. We will walk through the following steps: 1. Setup multiple journals and an account set in each journal 2. Post transactions to each journal 3. Query ledger entries across both journals ## Step 1: Multi Journal Setup We'll create two journals: **"Journal 1"** as the primary journal and **"Journal 2"** as the secondary journal. Then, we'll create two account sets, one for each journal, both with a credit-normal balance type. Next, create two accounts "Account A" and "Account B" and associate them with both account sets. Finally, create a transfer transaction code with the necessary parameters and transaction entries. **Request** ```graphql mutation MultiJournalSetup { j1: createJournal( input: { journalId: "41ee64d6-dcde-46dc-bdc5-c9164517402a" name: "Journal 1" description: "Primary journal" } ) { journalId } j2: createJournal( input: { journalId: "2d9355ac-1844-4378-8fae-7ccb104e98c9" name: "Journal 2" description: "Secondary journal" } ) { journalId } acct_set_1: createAccountSet( input: { accountSetId: "861f2709-5e62-4a97-8364-b4d3b0a3b31f" journalId: "41ee64d6-dcde-46dc-bdc5-c9164517402a" name: "Accounts" description: "Group entries in all accounts for primary journal" normalBalanceType: CREDIT } ) { accountSetId normalBalanceType } acct_set_2: createAccountSet( input: { accountSetId: "c21f202f-d92d-48e5-aaf0-d48865a98051" journalId: "2d9355ac-1844-4378-8fae-7ccb104e98c9" name: "Accounts" description: "Group entries in all accounts for secondary journal" normalBalanceType: CREDIT } ) { accountSetId normalBalanceType } acct_a: createAccount( input: { accountId: "80c06296-7afb-4725-8cdd-57c2ce881af2" code: "ACCT_A" name: "Account A" description: "Account A" normalBalanceType: CREDIT status: ACTIVE accountSetIds: [ "861f2709-5e62-4a97-8364-b4d3b0a3b31f" "c21f202f-d92d-48e5-aaf0-d48865a98051" ] } ) { accountId sets(first: 4) { nodes { accountSetId normalBalanceType } } } acct_b: createAccount( input: { accountId: "6d808949-437f-4f23-a345-b09225104808" code: "ACCT_B" name: "Account B" description: "Account B" normalBalanceType: CREDIT status: ACTIVE accountSetIds: [ "861f2709-5e62-4a97-8364-b4d3b0a3b31f" "c21f202f-d92d-48e5-aaf0-d48865a98051" ] } ) { accountId sets(first: 4) { nodes { accountSetId normalBalanceType } } } xfr: createTranCode( input: { tranCodeId: "8f50f1be-a6c5-4f42-a623-57db9ff9f543" code: "XFR" description: "Transfer money." params: [ { name: "effectiveDate", type: DATE } { name: "amount", type: DECIMAL } { name: "fromAccount" type: UUID description: "Account to send funds from." } { name: "toAccount" type: UUID description: "Recipient account. Required." } { name: "journal", type: UUID, description: "Journal ID. Required." } ] transaction: { journalId: "params.journal" effective: "params.effectiveDate" } entries: [ { accountId: "params.fromAccount" units: "params.amount" currency: "'USD'" entryType: "'XFR_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.toAccount" units: "params.amount" currency: "'USD'" entryType: "'XFR_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "j1": { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a" }, "j2": { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9" }, "acct_set_1": { "accountSetId": "861f2709-5e62-4a97-8364-b4d3b0a3b31f", "normalBalanceType": "CREDIT" }, "acct_set_2": { "accountSetId": "c21f202f-d92d-48e5-aaf0-d48865a98051", "normalBalanceType": "CREDIT" }, "acct_a": { "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "sets": { "nodes": [ { "accountSetId": "c21f202f-d92d-48e5-aaf0-d48865a98051", "normalBalanceType": "CREDIT" }, { "accountSetId": "861f2709-5e62-4a97-8364-b4d3b0a3b31f", "normalBalanceType": "CREDIT" } ] } }, "acct_b": { "accountId": "6d808949-437f-4f23-a345-b09225104808", "sets": { "nodes": [ { "accountSetId": "c21f202f-d92d-48e5-aaf0-d48865a98051", "normalBalanceType": "CREDIT" }, { "accountSetId": "861f2709-5e62-4a97-8364-b4d3b0a3b31f", "normalBalanceType": "CREDIT" } ] } }, "xfr": { "tranCodeId": "8f50f1be-a6c5-4f42-a623-57db9ff9f543" } } } ``` After completing this setup, our chart of accounts will look like this: | Name | Description | Normal Balance Type | Record Type | |-----------|---------------------------------------------------------|---------------------|-------------| | Account A | Account A | CREDIT | Account | | Account B | Account B | CREDIT | Account | | Accounts | Group entries in all accounts for the primary journal | CREDIT | AccountSet | | Accounts | Group entries in all accounts for the secondary journal | CREDIT | AccountSet | We'll also have a simple `XFR` tran code for moving money between accounts. ## Step 2: Post Transactions Next, let's post two transactions using the `XFR` transaction code created above to write some entries to each journal. In the first transaction, we'll move $11.33 from Account A to Account B in _Journal 1_. In the second transaction, we'll transfer $22.44 from Account B to Account A in _Journal 2_. **Request** ```graphql mutation PostTransactions { xfr_j1: postTransaction( input: { transactionId: "a3501651-3550-40a3-a5e4-a07d983348f8" tranCode: "XFR" params: { fromAccount: "80c06296-7afb-4725-8cdd-57c2ce881af2" toAccount: "6d808949-437f-4f23-a345-b09225104808" amount: "11.33" journal: "41ee64d6-dcde-46dc-bdc5-c9164517402a" effectiveDate: "2022-09-10" } } ) { transactionId entries(first: 10) { nodes { journalId accountId direction units currency } } } xfr_j2: postTransaction( input: { transactionId: "87e0c5f2-fc1e-4450-9129-aba1c00533c3" tranCode: "XFR" params: { fromAccount: "6d808949-437f-4f23-a345-b09225104808" toAccount: "80c06296-7afb-4725-8cdd-57c2ce881af2" amount: "22.44" journal: "2d9355ac-1844-4378-8fae-7ccb104e98c9" effectiveDate: "2022-09-11" } } ) { transactionId entries(first: 10) { nodes { journalId accountId direction units currency } } } } ``` **Response** ```json { "data": { "xfr_j1": { "transactionId": "a3501651-3550-40a3-a5e4-a07d983348f8", "entries": { "nodes": [ { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "direction": "DEBIT", "units": "11.33", "currency": "USD" }, { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "6d808949-437f-4f23-a345-b09225104808", "direction": "CREDIT", "units": "11.33", "currency": "USD" } ] } }, "xfr_j2": { "transactionId": "87e0c5f2-fc1e-4450-9129-aba1c00533c3", "entries": { "nodes": [ { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "6d808949-437f-4f23-a345-b09225104808", "direction": "DEBIT", "units": "22.44", "currency": "USD" }, { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "direction": "CREDIT", "units": "22.44", "currency": "USD" } ] } } } } ``` ## Step 3: Account Set Entries Finally, let's investigate the entries written in each journal by the transactions posted. We'll retrieve account sets 1 and 2 using their respective IDs. For each account set, we'll get the member accounts and entries. Also, we'll get the journals' balance (in USD). **Request** ```graphql query AccountSetEntries { acct_set_1: accountSet(id: "861f2709-5e62-4a97-8364-b4d3b0a3b31f") { members(first: 10) { nodes { ... on Account { accountId } } } entries(first: 20) { nodes { journalId accountId entryType layer direction units currency } } balance(currency: "USD") { journalId settled { normalBalance { units } drBalance { units } crBalance { units } } } } acct_set_2: accountSet(id: "c21f202f-d92d-48e5-aaf0-d48865a98051") { journalId members(first: 10) { nodes { ... on Account { accountId } } } entries(first: 20) { nodes { journalId accountId entryType layer direction units currency } } balance(currency: "USD") { journalId settled { normalBalance { units } drBalance { units } crBalance { units } } } } } ``` **Response** ```json { "data": { "acct_set_1": { "members": { "nodes": [ { "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2" }, { "accountId": "6d808949-437f-4f23-a345-b09225104808" } ] }, "entries": { "nodes": [ { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "6d808949-437f-4f23-a345-b09225104808", "entryType": "XFR_CR", "layer": "SETTLED", "direction": "CREDIT", "units": "11.33", "currency": "USD" }, { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "entryType": "XFR_DR", "layer": "SETTLED", "direction": "DEBIT", "units": "11.33", "currency": "USD" } ] }, "balance": { "journalId": "41ee64d6-dcde-46dc-bdc5-c9164517402a", "settled": { "normalBalance": { "units": "0.00" }, "drBalance": { "units": "11.33" }, "crBalance": { "units": "11.33" } } } }, "acct_set_2": { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "members": { "nodes": [ { "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2" }, { "accountId": "6d808949-437f-4f23-a345-b09225104808" } ] }, "entries": { "nodes": [ { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "80c06296-7afb-4725-8cdd-57c2ce881af2", "entryType": "XFR_CR", "layer": "SETTLED", "direction": "CREDIT", "units": "22.44", "currency": "USD" }, { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "accountId": "6d808949-437f-4f23-a345-b09225104808", "entryType": "XFR_DR", "layer": "SETTLED", "direction": "DEBIT", "units": "22.44", "currency": "USD" } ] }, "balance": { "journalId": "2d9355ac-1844-4378-8fae-7ccb104e98c9", "settled": { "normalBalance": { "units": "0.00" }, "drBalance": { "units": "22.44" }, "crBalance": { "units": "22.44" } } } } } } ``` For easier reading, here are the entries listed out: | Type | Journal | Account | Direction | Amount | |----------|-----------|-----------|-----------|--------| | `XFR_CR` | Journal 1 | Account B | `CREDIT` | $11.33 | | `XFR_DR` | Journal 1 | Account A | `DEBIT` | $11.33 | | `XFR_CR` | Journal 2 | Account A | `CREDIT` | $22.44 | | `XFR_DR` | Journal 2 | Account B | `DEBIT` | $22.44 | ## Summary We began by establishing two separate journals and creating account sets and accounts for each. We then created a transfer transaction code that allowed us to post transactions across these journals. By posting two sample transactions, we illustrated how balances are materialized in accounts and account sets across different journals. This tutorial provides a simplified into managing complex financial scenarios where multiple journals are required, such as consolidating the financial activity of multiple subsidiaries in a parent company or tracking transactions in various currencies. --- # Consuming Parquet pipeline files List and download near-real-time ledger change data in Parquet format. When enabled for your environment, Twisp writes committed ledger changes to tenant-isolated Parquet files and exposes them through the Files API. No managed Redshift warehouse is required. ## File layout Files use hourly UTC partitions: ```text warehouse/parquet/YYYY/MM/DD/HH//-NNNN.parquet ``` Available entities are `journal`, `account`, `account_context`, `account_set`, `account_set_member`, `tran_code`, `transaction`, `entry`, `balance`, `calculation`, `velocity_control`, `velocity_limit`, `transaction_exception`, and `workflow_execution`. List a narrow, preferably hourly, prefix: ```graphql query ListParquetFiles($pageToken: String) { files { listPage( keyPrefix: "warehouse/parquet/2026/07/29/10/entry/" pageSize: 250 pageToken: $pageToken ) { keys { key download { downloadURL } } nextPageToken } } } ``` Repeat the query with `nextPageToken` until it is `null`. The older `files.list` field is deprecated because it loads every matching object into one response. Select `download` when you need a five-minute presigned URL for each listed key; omit it when you only need the file inventory. You can also create a fresh download URL for any returned key with a mutation: ```graphql mutation DownloadParquetFile { files { createDownload(key: "warehouse/parquet/2026/07/29/10/entry/seed-000003-0000.parquet") { downloadURL downloadURLExpiration downloadHeaders contentType } } } ``` ## CDC semantics The files contain change data, not latest-state snapshots. Every committed record version carries the warehouse metadata columns `record_begin`, `record_rowid`, `record_status`, `record_tenantid`, and `record_version`. Process deletes according to `record_status` and deduplicate on `(record_rowid, record_version)`. A historical seed represents records exported at time T. The live pipeline should be enabled before creating the seed export. Seed and live files will then overlap slightly instead of leaving a gap; the normal version deduplication removes that overlap. The hour in a seed key is the export time. Live file hours are their delivery partitions. Consumers should checkpoint successfully processed object keys and continue polling new hourly prefixes. Raw delivery-stream inputs expire after 30 days. Parquet retention is configured separately by agreement with the customer. --- # Void and Post Transactions Learn how to replace transactions with Twisp's voidTransaction and postTransaction mutations. Many transactions have multi-step lifecycles where funds are first authorized at a pending layer and later settle. To the end user these transactions are logically a single event, but there may actually be multiple ledgering events that occur to the model the lifecycle of the transaction. ISO-8583 card authorizations are the canonical example, highly simplified: **Step 1: Authorize** ```mermaid flowchart TD subgraph Authorization C["Customer swipes card at merchant"] POS["Merchant POS"] NET["Network"] ISS["Issuer Bank Processor"] LEDGERP["Ledger entry: PENDING"] C --> POS POS -- "Auth request" --> NET NET -- "Auth request" --> ISS ISS -- "Check balances, Write PENDING" --> LEDGERP ISS -- "Auth response" --> NET NET -- "Auth response" --> POS end ``` **Step 2: Capture/Settle** ```mermaid flowchart TD subgraph Capture/Settlement POS2["Merchant POS (Capture)"] NET2["Network"] ISS2["Issuer Bank Processor"] LEDGERS["Ledger entry: SETTLED"] POS2 -- "Capture (settlement)" --> NET2 NET2 -- "Settle request" --> ISS2 ISS2 -- "Void PENDING, Write SETTLED" --> LEDGERS end ``` This tutorial walks through modeling that lifecycle in Twisp. You will: - define layered tran codes for pending and settled states - post an authorization, inspect correlated transactions, and understand the metadata that ties them together - void the authorization while posting the settled capture in a single mutation batch - automate transaction id generation via `VOID_AND_POST` workflow helper - make a tran code self-voiding with the scheduled-void workflow - build an activity feed index that hides void noise for end users ## Prerequisites - Access to Twisp’s Financial GraphQL API (for example via GraphiQL). - Journal and account identifiers for the ledger you want to write to. The examples below reuse the [demo IDs from the example setup script.](https://www.twisp.com/docs/tutorials/example-setup.md) - Familiarity with posting an initial transaction: voiding requires the original `transactionId`. Replace UUID defaults in the snippets with values from your environment when running them against a live ledger. ## Step 1 — Create layered tran codes Tran codes define the accounting logic that `postTransaction` reuses. We will create one code to place a hold on the PENDING layer and another to settle on the SETTLED layer. Both share a `correlationId` so they can be queried as a single lifecycle. ```graphql mutation CreateLifecycleTranCodes( $pendingTranCodeId: UUID! = "e62e2a14-ba73-11f0-a918-069b540ea27c" $settledTranCodeId: UUID! = "ec828824-ba73-11f0-a35f-069b540ea27c" ) { pending: createTranCode( input: { tranCodeId: $pendingTranCodeId code: "SAMPLE_TRANSFER_PENDING" description: "Place funds on hold at the pending layer." metadata: { category: "Card" } params: [ { name: "crAccount", type: UUID, description: "Account to credit." } { name: "drAccount", type: UUID, description: "Account to debit." } { name: "amount", type: DECIMAL, description: "Authorized amount." } { name: "currency", type: STRING, description: "ISO-4217 currency." } { name: "effective", type: DATE, description: "Authorization date." } { name: "journalId" type: UUID description: "Journal that records this flow." default: "c2881874-007e-43e1-85ef-c263e8e361aa" } { name: "correlationId" type: STRING description: "Identifier shared across the lifecycle." } { name: "metadata" type: JSON description: "Optional JSON payload for webhooks." default: "{}" } ] transaction: { journalId: "params.journalId" effective: "params.effective" correlationId: "params.correlationId" description: "'Authorization hold for ' + string(params.amount) + ' ' + params.currency" metadata: "params.metadata" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'AUTH_PENDING_DR'" direction: "DEBIT" layer: "PENDING" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'AUTH_PENDING_CR'" direction: "CREDIT" layer: "PENDING" } ] } ) { tranCodeId code } settled: createTranCode( input: { tranCodeId: $settledTranCodeId code: "SAMPLE_TRANSFER_SETTLED" description: "Post the settled capture and release the hold." metadata: { category: "Card" } params: [ { name: "crAccount", type: UUID, description: "Account to credit." } { name: "drAccount", type: UUID, description: "Account to debit." } { name: "amount", type: DECIMAL, description: "Captured amount." } { name: "currency", type: STRING, description: "ISO-4217 currency." } { name: "effective" type: DATE description: "Settlement posting date." } { name: "journalId" type: UUID description: "Journal that records this flow." } { name: "correlationId" type: STRING description: "Identifier shared across the lifecycle." } { name: "metadata" type: JSON description: "Optional JSON payload for webhooks." default: "{}" } ] transaction: { journalId: "params.journalId" effective: "params.effective" correlationId: "params.correlationId" description: "'Capture settled for ' + string(params.amount) + ' ' + params.currency" metadata: "params.metadata" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'AUTH_CAPTURE_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'AUTH_CAPTURE_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId code } } ``` Record the two `code` values—they tie directly to the mutations we use next. ## Step 2 — Post the pending authorization Use `postTransaction` with the pending tran code. Correlation links follow-on transactions back to this authorization. The `metadata` parameter must be JSON-encoded (pass a JSON string or variable). ```graphql mutation PostPendingAuthorization( $transactionId: UUID! = "0c970fb5-29e1-4c4f-87d0-b20557a19a5a" $correlationId: String! = "purchase-1001" $creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16" $debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9" $journalId: UUID! = "c2881874-007e-43e1-85ef-c263e8e361aa" ) { authorize: postTransaction( input: { transactionId: $transactionId tranCode: "SAMPLE_TRANSFER_PENDING" params: { crAccount: $creditAccountId drAccount: $debitAccountId journalId: $journalId amount: "25.00" currency: "USD" effective: "2025-01-07" correlationId: $correlationId metadata: { merchantMcc: "5812" merchantName:"Coffee Bar" } } } ) { transactionId correlationId effective entries(first: 2) { nodes { entryId accountId direction layer amount { units currency } } } } } ``` After posting, confirm the correlation in a read query: ```graphql query AuthorizationLifecycle( $journalId: String! = "c2881874-007e-43e1-85ef-c263e8e361aa" $correlationId: String! = "purchase-1001" ) { transactions( index: { name: CORRELATION_ID } where: { journalId: { eq: $journalId } correlationId: { eq: $correlationId } } first: 10 ) { nodes { transactionId voidOf voidedBy entries(first: 2) { nodes { layer amount { units currency } } } } } } ``` You should see one `PENDING` transaction at this stage. ## Step 3 — Void the hold and post the capture When settlement arrives, void the original transaction and immediately post the capture. Because both operations are mutations you can execute them in a single GraphQL document so the integration stays idempotent. ```graphql mutation CaptureAuthorization( $authorizationId: UUID! = "0c970fb5-29e1-4c4f-87d0-b20557a19a5a" $captureId: UUID! = "4d0d1fa5-4409-4f23-8b00-2eee8369bb98" $correlationId: String! = "purchase-1001" $creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16" $debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9" $journalId: UUID! = "c2881874-007e-43e1-85ef-c263e8e361aa" ) { voidPending: voidTransaction(id: $authorizationId) { transactionId voidOf correlationId } capture: postTransaction( input: { transactionId: $captureId tranCode: "SAMPLE_TRANSFER_SETTLED" params: { crAccount: $creditAccountId drAccount: $debitAccountId journalId: $journalId amount: "25.00" currency: "USD" effective: "2025-01-08" correlationId: $correlationId metadata: { merchantMcc: "5812" merchantName:"Coffee Bar" captureBatchId: "batch-450" } } } ) { transactionId correlationId voidOf entries(first: 2) { nodes { layer direction amount { units currency } } } } } ``` Re-run `AuthorizationLifecycle` and you will now see: - the original transaction with `voidedBy` populated - a void transaction with `voidOf` pointing back to the authorization - the settled capture on the `SETTLED` layer ## Step 4 — Post to a single identifier via `VOID_AND_POST` workflow Twisp’s transfer workflow wraps the pattern above. The workflow keeps state by `executionId` and uses the `VOID_AND_POST` task to post and void as needed. After creating the tran codes in Step 1, you can orchestrate the end-to-end lifecycle like this: ```graphql mutation WorkflowVoidAndPost( $executionId: UUID! = "5368ff5e-48b5-4c69-a6c3-d4efcf3804eb" $creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16" $debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9" $journalId: UUID! = "c2881874-007e-43e1-85ef-c263e8e361aa" $correlationId: String = "purchase-1002" ) { pending: workflow { execute( input: { workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4" task: "VOID_AND_POST" executionId: $executionId params: { tranCode: "SAMPLE_TRANSFER_PENDING" amount: "25.00" currency: "USD" effective: "2025-01-07" metadata: { merchantMcc: "5812" merchantName:"Coffee Bar" } crAccount: $creditAccountId drAccount: $debitAccountId journalId: $journalId correlationId: $correlationId } } ) { output { state } } } voidAndPost: workflow { execute( input: { workflowId: "c97010ac-f703-4112-8bb3-493ec0c2dfd4" task: "VOID_AND_POST" executionId: $executionId params: { tranCode: "SAMPLE_TRANSFER_SETTLED" amount: "25.00" currency: "USD" effective: "2025-01-08" metadata: { merchantMcc: "5812" merchantName:"Coffee Bar" captureBatchId: "batch-450" } crAccount: $creditAccountId drAccount: $debitAccountId journalId: $journalId correlationId: $correlationId } } ) { activities { action entityType entity { ... on Transaction { transactionId correlationId voidOf entries(first: 2) { nodes { layer amount { units currency } } } } } } } } } ``` > **Note:** > > Note the `workflowId` and `task` parameters are fixed values. ## Step 5 — Make a tran code self-voiding The scheduled-void workflow can be attached directly to a tran code. Every transaction posted with the code then schedules its own void without a second API call. The built-in workflow ID is `5986d918-15e2-48a2-b321-c05db0b0f7ca`. Its `SCHEDULE_VOID` task accepts `transactionId` and `when` parameters. Use the posted `transaction_id` as both the workflow execution ID and the transaction to void, and expose only the void time to callers: ```graphql mutation CreateSelfVoidingTranCode( $tranCodeId: UUID! = "e51a1377-3097-4a51-ab75-5782846f5aec" ) { createTranCode( input: { tranCodeId: $tranCodeId code: "TEMPORARY_TRANSFER" description: "Transfer that automatically voids at the requested time." params: [ { name: "crAccount", type: UUID } { name: "drAccount", type: UUID } { name: "amount", type: DECIMAL } { name: "currency", type: STRING } { name: "effective", type: DATE } { name: "voidAt", type: TIMESTAMP } ] transaction: { journalId: "uuid('c2881874-007e-43e1-85ef-c263e8e361aa')" effective: "params.effective" description: "'Temporary transfer'" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'TEMPORARY_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'TEMPORARY_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } ] workflow: { workflowId: "uuid('5986d918-15e2-48a2-b321-c05db0b0f7ca')" executionId: "transaction_id" task: "'SCHEDULE_VOID'" params: { transactionId: "transaction_id" when: "params.voidAt" } } } ) { tranCodeId code } } ``` Replace the journal UUID in the CEL expression with your journal ID. Then post normally and provide the scheduled time as an RFC 3339 timestamp: ```graphql mutation PostTemporaryTransfer( $transactionId: UUID! = "e10b3915-1853-47aa-b6b8-0cb17372392d" $creditAccountId: UUID! = "685fba2a-1ec6-4ae9-ace6-d9683d142c16" $debitAccountId: UUID! = "7c1afcde-7863-41b8-9688-72730f4d61f9" ) { postTransaction( input: { transactionId: $transactionId tranCode: "TEMPORARY_TRANSFER" params: { crAccount: $creditAccountId drAccount: $debitAccountId amount: "25.00" currency: "USD" effective: "2025-01-07" voidAt: "2030-01-08T12:00:00Z" } } ) { transactionId workflows(first: 1) { nodes { task context } } } } ``` The transaction and its schedule are committed together. At `voidAt`, Temporal runs the workflow's `VOID` task. If the transaction was already voided manually, the scheduled execution completes successfully and records the existing void transaction instead of failing. ## Step 6 — Build a clean activity feed End users expect to see a single line item even though the ledger contains three transactions (pending, void, and settled). Create a custom index that filters void/voided entries when you render an activity feed: ```graphql mutation CreateActivityFeedIndex { schema { createIndex( input: { name: "feed" on: Entry partition: [ { alias: "journal_id", value: "document.journal_id" } { alias: "account_id", value: "document.account_id" } ] sort: [ { sort: ASC, alias: "created", value: "document.created" } ] constraints: { isNotVoidEntry: "!document.is_void_entry" isNotVoidedEntry: "!document.is_voided_entry" } } ) { id } } } ``` Query the index with `entries(index: { name: CUSTOM, custom: { name: "feed" } }, ...)` to power a customer-facing feed that ignores internal bookkeeping noise. --- You now have a full lifecycle: create layered tran codes, post an authorization, void and capture, utilize workflows to maintain or automatically end a transaction lifecycle, and present a tidy history. Explore extending the workflow with additional states (for partial captures or declines) and add monitoring that alerts whenever a void happens outside of an expected settlement window. --- # Making API Requests with cURL In this tutorial, we will learn how to make requests against the Twisp GraphQL API using the curl command line tool. **Prerequisites** To complete this tutorial, you'll need: - The URL endpoint for the API - The `accountId` of your tenant - An authenticated JWT --- To make HTTPS requests to the Twisp GraphQL API using cURL, follow these steps: 1. Open a command-line interface (Terminal on macOS/Linux or Command Prompt on Windows). 2. Type in the following command: ```shell curl 'https://api..cloud.twisp.com/financial/v1/graphql' \ ``` This line specifies the URL to which the cURL request will be made. Replace `` with the AWS region for your ledger, e.g. `us-east-1`. 3. Add the required headers by typing the following lines: ```shell -H 'authorization: Bearer ' \ -H 'content-type: application/json, application/json' \ -H 'x-twisp-account-id: ' \ ``` Replace `` with your authenticated JWT and `` with your Twisp account ID for the tenant. The `-H` flag is used to specify custom headers. In this case, the headers being set are: - `authorization` for authentication purposes. - `content-type` to indicate the type of data being sent in the request body. - `x-twisp-account-id` for specifying the account ID to be used in the request. 4. Add the request payload by typing the following line: ```shell --data-raw '{"query":"query { accounts(index: { name: STATUS }, where: { status: { eq: "ACTIVE" } }, first: 5) { nodes { accountId code name } } }"}' ``` This line contains the `--data-raw` flag followed by a JSON string. This JSON string includes a GraphQL query to fetch accounts with the specified conditions (in this case, the first 5 active accounts). This is where you write your GraphQL operation. 5. After entering all the lines, the complete cURL request should look like: ```shell curl 'https://api..cloud.twisp.com/financial/v1/graphql' \ -H 'authorization: Bearer ' \ -H 'content-type: application/json, application/json' \ -H 'x-twisp-account-id: ' \ --data-raw '{"query":"query { accounts(index: { name: STATUS }, where: { status: { eq: "ACTIVE" } }, first: 5) { nodes { accountId code name } } }"}' ``` 6. Press Enter to execute the cURL request. If successful, you should receive a JSON response containing the requested data. --- # Querying Paginated Fields In this tutorial, we will learn to handle cursor-paginated fields in the GraphQL schema. ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). --- ## 1. Identify the connection field Any field that resolves to a `*Connection` type uses cursor-based pagination. This includes top-level queries like `entries` and `transactions`, as well as fields within object types like `Account.balances`. ## 2. Request edges and nodes To fetch data in pages, request the `edges` in the connection field. Each edge represents a link to a data node and may contain additional information about the relationship. Within each edge, request the `node` field to retrieve the actual data object. Here's a sample query for fetching the first 10 items of a field called `someConnection`: ```graphql query { someConnection(first: 10) { edges { node { id name } } } } ``` > **Note:** > > Connection types also contain a `nodes` field, which is just a way to more directly access the list of `node` objects in `edges`. It can be useful in cases where you don't need to query any other fields of the edge object. ## 3. Get the page info Alongside the edges, request the `pageInfo` object, which contains information about pagination. `pageInfo` includes the fields `hasNextPage`, `hasPreviousPage`, `startCursor`, and `endCursor`. Add the `pageInfo` field to your query: ```graphql query { someConnection(first: 10) { edges { node { id name } } pageInfo { hasNextPage endCursor } } } ``` ## 4. Paginate using cursors Cursors are opaque strings representing the position of an item in the list. Use the `after` argument in conjunction with the `first` argument to request the next set of _n_ items after the `endCursor`: ```graphql query { someConnection(first: 10, after: "") { edges { node { id name } } pageInfo { hasNextPage endCursor } } } ``` Replace `""` with the actual `endCursor` value from the previous `pageInfo`. ## 5. Iterate through pages Continue making requests using the updated `endCursor` until the `hasNextPage` field in `pageInfo` is `false`, indicating that there are no more pages to fetch. --- # Building Tran Codes In this tutorial, we will learn how to manage transaction codes using the GraphQL API. Transaction codes play a critical role in identifying and categorizing transactions in the ledger, and designing them is an important part of using Twisp. By following the steps outlined in this tutorial, you will be able to create and manage transaction codes in your own ledger. > **Task:** > > - Create a new tran code using the `createTranCode` mutation > - Update fields on an existing tran code using the `updateTranCode` mutation > - Get data about a tran code with the `tranCode` query > - Delete (lock) a tran code using the `deleteTranCode` mutation --- ## Prerequisites Before you start, you should have added accounts to your ledger. See the tutorial on [Setting Up Accounts](https://www.twisp.com/docs/tutorials/setting-up-accounts.md). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Create a new tran code To create a new tran code, use the `createTranCode` mutation. The `input` argument specifies the full configuration of the tran code and how transactions posted with this code will be structured, including all entries written to the ledger. Many of the fields in the `TranCodeInput` object (and its nested objects) allow for CEL expressions to be used, which will be evaluated during the `postTransaction` call and the results will be used in the creation of a transaction posted with the new transaction code. Note that the `params` field allows for the specification of parameters that can be used in CEL expressions for the transaction and entries fields. These parameters can be used to make the transaction code more flexible and dynamic, allowing for the creation of more complex transactions. The `transaction` and `entries` fields, respectively, act as templates for the [Transaction](https://www.twisp.com/docs/reference/graphql/types/object.md#transaction) and [Entries](https://www.twisp.com/docs/reference/graphql/types/object.md#entry) that are created when this tran code is used in a `postTransaction` call. Here's an example of creating a new tran code for a `BOOK_TRANSFER`: **Request** ```graphql mutation CreateTranCode( $tcBookTransferId: UUID! $journalGLIdExp: Expression! ) { createTranCode( input: { tranCodeId: $tcBookTransferId code: "BOOK_TRANSFER" description: "Book transfer between two internal accounts." metadata: { category: "Internal" } params: [ { name: "crAccount", type: UUID, description: "Account to credit." } { name: "drAccount", type: UUID, description: "Account to debit." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "currency" type: STRING description: "Currency used for transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } ] vars: { amount2: "decimal('1.00')", amount3: "this.amount2" } transaction: { journalId: $journalGLIdExp effective: "params.effective" description: "'Book transfer for $' + string(params.amount)" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } } } ``` **Variables** ```json { "journalGLIdExp": "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')", "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` Once created, transactions can be posted using this transaction code by providing the required fields in a `TransactionInput` object. The inputs for a `postTransaction` using this tran code must include fields for the `crAccount`, `drAccount`, `amount`, `currency`, and `effective`. > **Note:** > > Designing tran codes is one of the most important parts of using Twisp, and it can take some time to familiarize yourself with the process. > > Read more about tran codes on the [Encoded Transactions](https://www.twisp.com/docs/accounting-core/encoded-transactions.md) page. ## Query tran codes The `tranCode` query can be used to read back an existing tran code using its `tranCodeId`. Here is an example query: **Request** ```graphql query GetBookTransferTranCode { tranCode(id: "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855") { code description params { name type description } transaction { journalId effective correlationId description } entries { accountId layer direction units currency } status metadata } } ``` **Response** ```json { "data": { "tranCode": { "code": "BOOK_TRANSFER", "description": "Book transfer between two internal accounts.", "params": [ { "name": "crAccount", "type": "UUID", "description": "Account to credit." }, { "name": "drAccount", "type": "UUID", "description": "Account to debit." }, { "name": "amount", "type": "DECIMAL", "description": "Amount with decimal, e.g. `1.23`." }, { "name": "currency", "type": "STRING", "description": "Currency used for transaction." }, { "name": "effective", "type": "DATE", "description": "Effective date for transaction." } ], "transaction": { "journalId": "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')", "effective": "params.effective", "correlationId": "''", "description": "'Book transfer for $' + string(params.amount)" }, "entries": [ { "accountId": "params.drAccount", "layer": "SETTLED", "direction": "DEBIT", "units": "params.amount", "currency": "params.currency" }, { "accountId": "params.crAccount", "layer": "SETTLED", "direction": "CREDIT", "units": "params.amount", "currency": "params.currency" } ], "status": "ACTIVE", "metadata": { "category": "Internal" } } } } ``` This query will respond with the fields for the `BOOK_TRANSFER` tran code created above, identified by its UUID. The `params` field in the response includes information about the parameters required by this transaction code, such as the name, type, and description of each parameter. The `transaction` field in the response includes details about the transaction that is created when this transaction code is used. The `entries` field in the response includes information about the entries that are created when this transaction code is used. Finally, the response includes the status of the transaction code, which can be either `ACTIVE` or `LOCKED`, and any metadata associated with the transaction code. ## Modify an existing tran code To modify an existing tran code, use the `updateTranCode` mutation. Provide the tran code's `id` and the fields to update in the `TranCodeUpdateInput` input object. You can only modify a subset of fields for data integrity purposes. Here's an example of modifying the description of an existing tran code: **Request** ```graphql mutation UpdateTranCode($tcBookTransferId: UUID!) { updateTranCode( id: $tcBookTransferId input: { description: "Book transfer between two customer wallet accounts." vars: { hello: "string('world')" } entries: [ { accountId: "params.drAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" metadata: "{'first': 1}" } { accountId: "params.crAccount" units: "params.amount" currency: "params.currency" entryType: "'BOOK_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" metadata: "{'second':2}" } ] } ) { tranCodeId description entries { metadata } vars history(first: 2) { nodes { version description vars } } } } ``` **Response** ```json { "data": { "updateTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855", "description": "Book transfer between two customer wallet accounts.", "entries": [ { "metadata": "{'first': 1}" }, { "metadata": "{'second':2}" } ], "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2", "hello": "string('world')" }, "history": { "nodes": [ { "version": 2, "description": "Book transfer between two customer wallet accounts.", "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2", "hello": "string('world')" } }, { "version": 1, "description": "Book transfer between two internal accounts.", "vars": { "amount2": "decimal('1.00')", "amount3": "this.amount2" } } ] } } } } ``` **Variables** ```json { "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` This operation updates the description of a transaction code. The response includes the UUID and the updated description of the transaction code, as well as the history of the changes made to the transaction code. ## Lock a tran code Because Twisp is an immutable database, we cannot fully "delete" a tran code. Instead, Twisp marks the tran code's status as `LOCKED`, which prevents transactions from posting using this version of tran code. To delete (lock) an tran code, we can use the `deleteTranCode` mutation. We need to provide the `id` of the tran code we want to delete. Here's an example mutation: **Request** ```graphql mutation DeleteTranCode($tcBookTransferId: UUID!) { deleteTranCode(id: $tcBookTransferId) { tranCodeId status } } ``` **Response** ```json { "data": { "deleteTranCode": { "tranCodeId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855", "status": "LOCKED" } } } ``` **Variables** ```json { "tcBookTransferId": "e0ffa94d-ca03-4ae2-aa69-cbaaa21d3855" } ``` Once the tran code is deleted, this mutation returns two fields: `tranCodeId` and `status`. `tranCodeId` is a UUID that uniquely identifies the deleted tran code, while `status` is the current status of the tran code (i.e. `LOCKED`). This mutation is useful if you need to remove an tran code that is no longer needed or was created in error. ## Conclusion This tutorial covered the basics of creating, modifying, and deleting transaction codes. Transaction codes play a critical role in identifying and categorizing transactions in the ledger, and designing them is an important part of using Twisp. By following the steps outlined in this tutorial, you should be able to create and manage transaction codes in your own ledger. --- # Implementing Custom Calculations Learn how to define, create, and use custom calculations in Twisp to track specialized balances (e.g., by effective date or metadata tags) and query them efficiently using the GraphQL API. Custom calculations allow you to define specialized ways to aggregate and query balances based on dimensions beyond the standard `accountId`, `journalId`, and `currency`. This enables tracking balances grouped by criteria like effective date, specific metadata tags, or other transaction/entry attributes relevant to your business logic. By the end of this tutorial, you will be able to design and create a custom calculation, and then query the specialized balances it generates. > **Task:** > > - Design a custom calculation with specific dimensions using CEL. > - Create the calculation using the `createCalculation` mutation. > - Query the calculated balances using the `balances` query with the `CALCULATION` index. > - (Optional) Attach a `LOCAL` scope calculation to specific accounts or sets. --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Designing a Custom Calculation Before creating a calculation, you need to define how you want to group and filter balances. Let's design a calculation to track balances *per effective date* for each account. Key components of a calculation definition: * **`calculationId`:** A unique UUID you provide to identify this calculation. * **`code`:** A unique, human-readable string code for the calculation (e.g., `PER_EFFECTIVE_DATE`). * **`description`:** Explains the purpose of the calculation. * **`scope`:** * `GLOBAL`: The calculation applies to *all* accounts and sets within the journal(s). This is the default. * `LOCAL`: The calculation *only* applies to accounts or sets where it has been explicitly attached using the `attachCalculation` mutation. * **`dimensions`:** An array defining *how* the balance is indexed or grouped. Each dimension requires: * `alias`: A name for the dimension (e.g., `effectiveDate`). * `value`: A [CEL expression](https://twisp.com/docs/reference/cel) referencing `context.vars` (which contains the `entry`, `transaction`, `account`, etc. being processed) to get the dimension's value (e.g., `context.vars.transaction.effective` to group by the transaction's effective date). * **`condition` (Optional):** A CEL expression that must evaluate to `true` for an entry to be included in this calculation. If omitted, all entries matching the dimensions are included. Example: `context.vars.entry.amount.units() > decimal('0.0')` would only include entries with positive amounts. For our example (tracking balance per effective date): * **`calculationId`:** `"5867b5dd-fc69-416c-80f5-62e8a53610d5"` (Use your own unique UUID) * **`code`:** `PER_EFFECTIVE_DATE` * **`description`:** `Track balances per effective date in an account.` * **`scope`:** `GLOBAL` (applies everywhere by default) * **`dimensions`:** `alias: "effectiveDate"`, `value: "context.vars.transaction.effective"` * **`condition`:** None (include all entries) ## Create the Custom Calculation Use the `createCalculation` mutation with the parameters defined above. ```graphql mutation CreateCalculationPerEffectiveDate { createCalculation( input: { calculationId: "5867b5dd-fc69-416c-80f5-62e8a53610d5" # Use your own UUID code: "PER_EFFECTIVE_DATE" description: "Track balances per effective date in an account." scope: GLOBAL dimensions: [ { alias: "effectiveDate" value: "context.vars.transaction.effective" } ] # condition: "context.vars.entry.layer == 'SETTLED'" # Optional example condition } ) { calculationId code description scope dimensions { alias value } condition version created modified } } ``` ```json { "data": { "createCalculation": { "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "code": "PER_EFFECTIVE_DATE", "description": "Track balances per effective date in an account.", "scope": "GLOBAL", "dimensions": [ { "alias": "effectiveDate", "value": "context.vars.transaction.effective" } ], "condition": null, "version": 1, "created": "2023-10-27T12:00:00Z", "modified": "2023-10-27T12:00:00Z" } } } ``` This mutation creates the `PER_EFFECTIVE_DATE` calculation. The response confirms its structure. Twisp will now start computing balances grouped by `accountId`, `journalId`, `currency`, *and* `effectiveDate`. ## Query Using the Custom Calculation To retrieve balances computed by a custom calculation, use the standard [`balances`](https://www.twisp.com/docs/reference/graphql/queries.md#balances) query, specifying `index: { name: CALCULATION }`. In the `where` clause, you **must** provide: * The [`accountId`](https://www.twisp.com/docs/reference/graphql/types/input.md#balance-filter-input:account-id) (or `accountSetId`). * The [`calculationId`](https://www.twisp.com/docs/reference/graphql/types/input.md#balance-filter-input:calculation-id) of your custom calculation. * A [`dimension`](https://www.twisp.com/docs/reference/graphql/types/input.md#balance-filter-input:dimension) filter matching the structure defined in your calculation. Provide the specific dimension `alias`(es) and the `value`(s) you want to query (e.g., a specific date for the `effectiveDate` alias). ```graphql query GetCalculationBalanceForDate($accountId: UUID!, $calcId: UUID!, $journalId: UUID) { balances( index: { name: CALCULATION } where: { accountId: { eq: $accountId } journalId: { eq: $journalId } # Optional: Specify journal or uses default calculationId: { eq: $calcId } # ID of the custom calculation dimension: { # Alias must match the one defined in the calculation effectiveDate: "2023-09-21" # Specific dimension value to query } currency: { eq: "USD" } # Optional: Filter by currency if needed } first: 10 ) { nodes { accountId journalId currency calculationId # Confirms which calculation produced this balance dimensions # Shows the specific dimension values for this balance record settled { normalBalance { units } } # Query other balance layers (pending, encumbrance) or fields as needed } pageInfo { hasNextPage endCursor } } } ``` **Variables for the query:** ```json { "accountId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "calcId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` **Example Response:** ```json { "data": { "balances": { "nodes": [ { "accountId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "currency": "USD", "calculationId": "5867b5dd-fc69-416c-80f5-62e8a53610d5", "dimensions": { "effectiveDate": "2023-09-21" }, "settled": { "normalBalance": { "units": "5.25" } } } // ... other balances matching the criteria might appear if pagination allows ], "pageInfo": { "hasNextPage": false, "endCursor": "..." } } } } ``` This query retrieves the balance record specifically calculated for the account `"1fd1dd3e-..."`, using the `"PER_EFFECTIVE_DATE"` calculation, for the exact effective date `"2023-09-21"`. The `dimensions` field in the response confirms the `effectiveDate` value associated with this particular balance record. ## Attaching Calculations (`LOCAL` Scope Only) If you created a calculation with `scope: LOCAL`, it only computes balances for accounts or sets where it's explicitly attached. `GLOBAL` calculations apply automatically and do not need attaching. Use the [`attachCalculation`](https://www.twisp.com/docs/reference/graphql/mutations.md#attach-calculation) mutation: * Provide the `calculationId` of the `LOCAL` calculation. * Provide the `accountId` of the target [Account](https://www.twisp.com/docs/reference/graphql/types/object.md#account) or [AccountSet](https://www.twisp.com/docs/reference/graphql/types/object.md#account-set). * Optionally provide the `journalId` if attaching to an `Account` and not using the default journal (ignored for `AccountSet`). ```graphql # Example: Attaching a LOCAL calculation to an account mutation AttachLocalCalcToAccount($calcId: UUID!, $accountId: UUID!, $journalId: UUID) { attachCalculation( input: { calculationId: $calcId # ID of the LOCAL calculation accountId: $accountId # ID of the Account or AccountSet to attach to journalId: $journalId # Optional: Specify journal for Account attachment } ) { # Response confirms attachment details calculationId accountId journalId calculation { code } account { name } } } ``` **Variables for the mutation:** ```json { "calcId": "YOUR_LOCAL_CALCULATION_ID", "accountId": "YOUR_TARGET_ACCOUNT_ID", "journalId": "YOUR_JOURNAL_ID" } ``` > **Note:** > > Creating a GLOBAL calculation or attaching a LOCAL calculation will initiate a backfill of that calculation using historical entries. It may take a few minutes for _new_ entries to show up in the balance while the backfill is ongoing. ## Conclusion In this tutorial, you learned how to design custom calculations by defining dimensions and optional conditions using CEL. You created a calculation using the `createCalculation` mutation and queried its specialized balances via the `balances` query with the `CALCULATION` index and dimension filters. Finally, you saw how to attach `LOCAL` scope calculations to specific accounts or sets using `attachCalculation`. Custom calculations offer powerful flexibility for tracking and querying balances based on criteria specific to your application's needs, enabling more granular financial insights and reporting. --- # Example Setup Use this example ledger configuration to start exploring Twisp and learning how to build your accounting system. This setup is for an example budgeting app called "Budget Buddy". Think of it as a mashup of a budgeting tool like [Mint](https://mint.intuit.com/) or [YNAB](https://www.ynab.com/) with a social wallet app like [Venmo](https://venmo.com/). **The full setup GraphQL code can be found at the bottom of this page.** ## Account Structure Budget Buddy's chart of accounts includes account sets for each user, containing that user's wallet, budget accounts, and linked financial accounts like checking or credit cards. It also has offset and settlement account. ### User Accounts ```mermaid graph BT bert[/Bert\] ernie[/Ernie\] users[/Users\] bert & ernie --> users ``` ```mermaid graph BT bert[/Bert\] bert_budget[Bert's Budget] bert_cash[Bert's Cash Acct.] bert_checking[Bert's Checking Acct.] bert_wallet[Bert's Wallet] bert_budget & bert_cash & bert_checking & bert_wallet --> bert ``` ```mermaid graph BT ernie[/Ernie\] ernie_budget[Ernie's Budget] ernie_checking[Ernie's Checking Acct.] ernie_credit_card[Ernie's Credit Card Acct.] ernie_wallet[Ernie's Wallet] ernie_budget & ernie_checking & ernie_credit_card & ernie_wallet --> ernie ``` ### Settlement Accounts ```mermaid graph BT settlement[/Settlement\] settlement_card[Card Settlement] settlement_cash[Cash Settlement] settlement_checking[Checking Settlement] settlement_card & settlement_cash & settlement_checking --> settlement ``` ### Additional Sets Additional account sets are used to roll up wallet and budget accounts: ```mermaid graph BT budgets[/Budgets\] wallets[/Wallets\] bert_budget[Bert's Budget] bert_wallet[Bert's Wallet] ernie_budget[Ernie's Budget] ernie_wallet[Ernie's Wallet] budget_offset[Budget Offset] budget_offset & bert_budget & ernie_budget --> budgets bert_wallet & ernie_wallet --> wallets ``` ## Tran Codes The setup defines several `TranCodes`: - `ALLOC_BUDGET`: Allocates an amount to a specific budget, creating a credit entry in the account and a debit entry in the budget offset account. - `DEALLOC_BUDGET`: Deallocates funds from a budget, creating a credit entry in the budget offset account and a debit entry in the account. - `ASSIGN_TO_BUDGET`: Assigns a transaction to a particular budget, creating a credit entry in the account and a debit entry in the budget offset account. - `RECORD_TX`: Records a transaction between two accounts, creating a debit entry in one account and a credit entry in another. - `RECORD_PENDING_TX`: Records a pending transaction between two accounts, creating a debit entry in one account and a credit entry in another. - `RECORD_SETTLE_PENDING_TX`: Records the settlement of a pending transaction, creating a credit entry in the corresponding account and a debit entry in the settlement account. - `WALLET_TRANSFER`: Transfers funds between two wallets, creating a debit entry in one wallet and a credit entry in another. - `WALLET_DEPOSIT`: Deposits funds into a wallet, creating a debit entry in the wallet account and a credit entry in the checking account. - `WALLET_WITHDRAW`: Withdraws funds from a wallet, creating a debit entry in the checking account and a credit entry in the wallet account. Each `TranCode` defines the entries that should be created in the ledger when the transaction is processed, as well as any metadata that should be associated with the transaction. ## GraphQL **Request** ```graphql mutation SetupAccounts( $journalId: UUID! $journalIdExpression: Expression! $set_bertId: UUID! $set_budgetsId: UUID! $set_ernieId: UUID! $set_settlementId: UUID! $set_usersId: UUID! $set_walletsId: UUID! $bertBudgetId: UUID! $bertCashId: UUID! $bertCheckingId: UUID! $bertWalletId: UUID! $budgetOffsetId: UUID! $budgetOffsetIdExpression: Expression! $ernieBudgetId: UUID! $ernieCheckingId: UUID! $ernieCreditCardId: UUID! $ernieWalletId: UUID! $settlementCardId: UUID! $settlementCashId: UUID! $settlementCheckingId: UUID! $tc_allocBudgetId: UUID! $tc_deallocBudgetId: UUID! $tc_recordTxId: UUID! $tc_recordPendingTxId: UUID! $tc_recordSettlePendingTxId: UUID! $tc_assignToBudgetId: UUID! $tc_walletTransferId: UUID! $tc_walletDepositId: UUID! $tc_walletWithdrawId: UUID! ) { gl: createJournal( input: { journalId: $journalId, name: "GL", description: "General Ledger" } ) { journalId } schema { createIndex( input: { name: "TRANSACTION.BUDGET_CATEGORY" on: Transaction unique: false partition: [ { alias: "userAccountId" value: "string(document.metadata.userAccountId)" } { alias: "budgetCategory" value: "string(document.metadata.budgetCategory)" } ] sort: [ { alias: "budgetCategory" value: "string(document.metadata.budgetCategory)" sort: ASC } ] constraints: { hasCategory: "has(document.metadata.budgetCategory)" hasUserAccountId: "has(document.metadata.userAccountId)" } } ) { name on unique } } acct_set_bert: createAccountSet( input: { accountSetId: $set_bertId journalId: $journalId name: "Bert" description: "Bert's account" normalBalanceType: DEBIT } ) { accountSetId name } acct_set_budgets: createAccountSet( input: { accountSetId: $set_budgetsId journalId: $journalId name: "Budgets" description: "All budget accounts" normalBalanceType: CREDIT } ) { accountSetId name } acct_set_ernie: createAccountSet( input: { accountSetId: $set_ernieId journalId: $journalId name: "Ernie" description: "Ernie's account." normalBalanceType: DEBIT } ) { accountSetId name } acct_set_settlement: createAccountSet( input: { accountSetId: $set_settlementId journalId: $journalId name: "Settlement" description: "All settlement accounts." normalBalanceType: CREDIT } ) { accountSetId name } acct_set_users: createAccountSet( input: { accountSetId: $set_usersId journalId: $journalId name: "Users" description: "All users' accounts." normalBalanceType: DEBIT } ) { accountSetId name } acct_set_wallets: createAccountSet( input: { accountSetId: $set_walletsId journalId: $journalId name: "Wallets" description: "All customer wallets." normalBalanceType: DEBIT } ) { accountSetId name } acct_bert_budget: createAccount( input: { accountId: $bertBudgetId code: "BERT.BUDGET" name: "Bert's Budget" description: "Bert's budgeting account" normalBalanceType: CREDIT accountSetIds: [$set_bertId, $set_budgetsId] status: ACTIVE } ) { accountId code name } acct_bert_cash: createAccount( input: { accountId: $bertCashId code: "BERT.CASH" name: "Bert's Cash Acct." description: "Bert's Cash" normalBalanceType: DEBIT accountSetIds: [$set_bertId] status: ACTIVE } ) { accountId code name } acct_bert_checking: createAccount( input: { accountId: $bertCheckingId code: "BERT.CHECKING" name: "Bert's Checking Acct." description: "Bert's Checking Acct." normalBalanceType: DEBIT accountSetIds: [$set_bertId] status: ACTIVE } ) { accountId code name } acct_bert_wallet: createAccount( input: { accountId: $bertWalletId code: "BERT.WALLET" name: "Bert's Wallet" description: "Bert's wallet" normalBalanceType: DEBIT accountSetIds: [$set_bertId, $set_walletsId] status: ACTIVE } ) { accountId code name } acct_budget_offset: createAccount( input: { accountId: $budgetOffsetId code: "BUDGET.OFFSET" name: "Budget Offset" description: "Offset accounts for budgeting." normalBalanceType: CREDIT accountSetIds: [$set_budgetsId] status: ACTIVE } ) { accountId code name } acct_ernie_budget: createAccount( input: { accountId: $ernieBudgetId code: "ERNIE.BUDGET" name: "Ernie's Budget" description: "Ernie's budgeting account" normalBalanceType: CREDIT accountSetIds: [$set_ernieId, $set_budgetsId] status: ACTIVE } ) { accountId code name } acct_ernie_checking: createAccount( input: { accountId: $ernieCheckingId code: "ERNIE.CHECKING" name: "Ernie's Checking Acct." description: "" normalBalanceType: DEBIT accountSetIds: [$set_ernieId] status: ACTIVE } ) { accountId code name } acct_ernie_credit_card: createAccount( input: { accountId: $ernieCreditCardId code: "ERNIE.CREDIT_CARD" name: "Ernie's Credit Card Acct." description: "Ernie's Credit Card Acct." normalBalanceType: CREDIT accountSetIds: [$set_ernieId] status: ACTIVE } ) { accountId code name } acct_ernie_wallet: createAccount( input: { accountId: $ernieWalletId code: "ERNIE.WALLET" name: "Ernie's Wallet" description: "Ernie's wallet" normalBalanceType: DEBIT accountSetIds: [$set_ernieId, $set_walletsId] status: ACTIVE } ) { accountId code name } acct_settlement_card: createAccount( input: { accountId: $settlementCardId code: "SETTLEMENT.CARD" name: "Card Settlement" description: "Settlement account for credit card transactions" normalBalanceType: CREDIT accountSetIds: [$set_settlementId] status: ACTIVE } ) { accountId code name } acct_settlement_cash: createAccount( input: { accountId: $settlementCashId code: "SETTLEMENT.CASH" name: "Cash Settlement" description: "Settlement account for cash transactions" normalBalanceType: CREDIT accountSetIds: [$set_settlementId] status: ACTIVE } ) { accountId code name } acct_settlement_checking: createAccount( input: { accountId: $settlementCheckingId code: "SETTLEMENT.CHECKING" name: "Checking Settlement" description: "Settlement account for checking transactions" normalBalanceType: CREDIT accountSetIds: [$set_settlementId] status: ACTIVE } ) { accountId code name } tc_alloc_budget: createTranCode( input: { tranCodeId: $tc_allocBudgetId code: "ALLOC_BUDGET" description: "Allocate an amount to a specific budget." params: [ { name: "account" type: UUID description: "User's budget account ID." } { name: "amount" type: DECIMAL description: "Amount to allocate to budget." } { name: "effective", type: DATE, description: "Current date." } { name: "category" type: STRING description: "Budget category to allocate to: 'Discretionary', 'Expenses', etc." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Allocate ' + string(params.amount) + ' to budget \"' + string(params.category) + '\" for user with account ID: ' + string(params.account)" metadata: "{ 'userAccountId': string(params.account), 'budgetCategory': params.category }" } entries: [ { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'ALLOC_BUDGET_CR'" direction: "CREDIT" layer: "ENCUMBRANCE" } { accountId: $budgetOffsetIdExpression units: "params.amount" currency: "'USD'" description: "''" entryType: "'ALLOC_BUDGET_DR'" direction: "DEBIT" layer: "ENCUMBRANCE" } ] } ) { tranCodeId code } tc_dealloc_budget: createTranCode( input: { tranCodeId: $tc_deallocBudgetId code: "DEALLOC_BUDGET" description: "Deallocate funds from a budget." params: [ { name: "account" type: UUID description: "User's budget account ID." } { name: "amount" type: DECIMAL description: "Amount to deallocate from budget." } { name: "effective", type: DATE, description: "Current date." } { name: "category" type: STRING description: "Budget category to deallocate from: 'Discretionary', 'Expenses', etc." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Deallocate ' + string(params.amount) + ' to budget \"' + string(params.category) + '\" for user with account ID: ' + string(params.account)" metadata: "{ 'userAccountId': string(params.account), 'budgetCategory': params.category }" } entries: [ { accountId: $budgetOffsetIdExpression units: "params.amount" currency: "'USD'" description: "''" entryType: "'DEALLOC_BUDGET_CR'" direction: "CREDIT" layer: "ENCUMBRANCE" } { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'DEALLOC_BUDGET_DR'" direction: "DEBIT" layer: "ENCUMBRANCE" } ] } ) { tranCodeId code } tc_assign_to_budget: createTranCode( input: { tranCodeId: $tc_assignToBudgetId code: "ASSIGN_TO_BUDGET" description: "Assign a transaction to a particular budget." params: [ { name: "account" type: UUID description: "User's budget account ID." } { name: "amount" type: DECIMAL description: "Amount to assign to budget." } { name: "effective", type: DATE, description: "Current date." } { name: "category" type: STRING description: "Budget category to deallocate from: 'Discretionary', 'Expenses', etc." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Assign ' + string(params.amount) + ' to budget \"' + string(params.category) + '\" for user with account ID: ' + string(params.account)" metadata: "{ 'userAccountId': string(params.account), 'budgetCategory': params.category }" } entries: [ { accountId: $budgetOffsetIdExpression units: "params.amount" currency: "'USD'" description: "''" entryType: "'ASSIGN_TO_BUDGET_CR'" direction: "CREDIT" layer: "ENCUMBRANCE" } { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'ASSIGN_TO_BUDGET_DR'" direction: "DEBIT" layer: "ENCUMBRANCE" } ] } ) { tranCodeId code } tc_record_tx: createTranCode( input: { tranCodeId: $tc_recordTxId code: "RECORD_TX" description: "Record a transaction." params: [ { name: "account", type: UUID, description: "User's asset account ID." } { name: "settlementAccount" type: UUID description: "Settlement account ID to use, determined by the transaction type. E.g. for card transactions, use the SETTLEMENT.CARD account." } { name: "amount", type: DECIMAL, description: "Amount of transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } { name: "transactionType" type: STRING description: "Type of transaction: ACH Debit, Check Deposit, etc." } { name: "isDebit" type: BOOLEAN description: "If true (default), debit the account specified. If false, credit the account." default: true } { name: "correlationId" type: STRING description: "Correlation identifier to group transactions in this sequence." default: "_unspecified_" } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Record ' + string(params.isDebit ? 'debit' : 'credit') + ' transaction for user with account ID: ' + string(params.account)" metadata: "{ 'transactionType': string(params.transactionType) }" correlationId: "string(params.correlationId != '_unspecified_' ? params.correlationId : uuid.New())" } entries: [ { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "SETTLED" } { accountId: "params.settlementAccount" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } tc_record_pending_tx: createTranCode( input: { tranCodeId: $tc_recordPendingTxId code: "RECORD_PENDING_TX" description: "Record a pending transaction." params: [ { name: "account", type: UUID, description: "User's asset account ID." } { name: "settlementAccount" type: UUID description: "Settlement account ID to use, determined by the transaction type. E.g. for card transactions, use the SETTLEMENT.CARD account." } { name: "amount", type: DECIMAL, description: "Amount of transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } { name: "transactionType" type: STRING description: "Type of transaction: ACH Debit, Check Deposit, etc." } { name: "isDebit" type: BOOLEAN description: "If true (default), debit the account specified. If false, credit the account." default: true } { name: "correlationId" type: STRING description: "Correlation identifier to group transactions in this sequence." default: "_unspecified_" } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Record pending ' + string(params.isDebit ? 'debit' : 'credit') + ' transaction for user with account ID: ' + string(params.account)" metadata: "{ 'transactionType': string(params.transactionType) }" correlationId: "string(params.correlationId != '_unspecified_' ? params.correlationId : uuid.New())" } entries: [ { accountId: "params.account" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_PENDING_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "PENDING" } { accountId: "params.settlementAccount" units: "params.amount" currency: "'USD'" description: "''" entryType: "'RECORD_PENDING_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "PENDING" } ] } ) { tranCodeId code } tc_record_settle_pending_tx: createTranCode( input: { tranCodeId: $tc_recordSettlePendingTxId code: "RECORD_SETTLE_PENDING_TX" description: "Settle a recorded pending transaction." params: [ { name: "account", type: UUID, description: "User's asset account ID." } { name: "settlementAccount" type: UUID description: "Settlement account ID to use, determined by the transaction type. E.g. for card transactions, use the SETTLEMENT.CARD account." } { name: "settledAmount" type: DECIMAL description: "Amount of settled transaction." } { name: "originalAmount" type: DECIMAL description: "Amount of original (pending) transaction." } { name: "effective" type: DATE description: "Effective date for transaction." } { name: "transactionType" type: STRING description: "Type of transaction: ACH Debit, Check Deposit, etc." } { name: "isDebit" type: BOOLEAN description: "If true (default), debit the account specified. If false, credit the account." default: true } { name: "correlationId" type: STRING description: "Correlation identifier to group transactions in this sequence." default: "_unspecified_" } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Settle recorded pending ' + string(params.isDebit ? 'debit' : 'credit') + ' transaction for user with account ID: ' + string(params.account)" metadata: "{ 'transactionType': string(params.transactionType) }" correlationId: "string(params.correlationId != '_unspecified_' ? params.correlationId : uuid.New())" } entries: [ { accountId: "params.account" units: "params.settledAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "SETTLED" } { accountId: "params.settlementAccount" units: "params.settledAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.originalAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_PENDING_TX_'+ string(params.isDebit ? 'CR' : 'DR')" # opposite direction direction: "params.isDebit ? CREDIT : DEBIT" layer: "PENDING" } { accountId: "params.settlementAccount" units: "params.originalAmount" currency: "'USD'" description: "''" entryType: "'RECORD_SETTLE_PENDING_TX_'+ string(params.isDebit ? 'DR' : 'CR')" direction: "params.isDebit ? DEBIT : CREDIT" layer: "PENDING" } ] } ) { tranCodeId code } tc_wallet_transfer: createTranCode( input: { tranCodeId: $tc_walletTransferId code: "WALLET_TRANSFER" description: "Transfer funds between wallets." params: [ { name: "fromWallet" type: UUID description: "User's wallet account to credit." } { name: "toWallet" type: UUID description: "User's wallet account to debit." } { name: "amount", type: DECIMAL, description: "Amount to transfer." } { name: "effective", type: DATE, description: "Date of transfer." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Transfer ' + string(params.amount) + ' from wallet ' + string(params.fromWallet) + ' to wallet ' + string(params.toWallet)" } entries: [ { accountId: "params.fromWallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.toWallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } tc_wallet_deposit: createTranCode( input: { tranCodeId: $tc_walletDepositId code: "WALLET_DEPOSIT" description: "Deposit funds into a user's wallet account from their checking account." params: [ { name: "wallet", type: UUID, description: "User's wallet account." } { name: "checking" type: UUID description: "User's checking account." } { name: "amount" type: DECIMAL description: "Amount to move into wallet." } { name: "effective", type: DATE, description: "Date of deposit." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Transfer ' + string(params.amount) + ' from checking account ' + string(params.checking) + ' to wallet ' + string(params.wallet)" } entries: [ { accountId: "params.checking" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.wallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } tc_wallet_withdraw: createTranCode( input: { tranCodeId: $tc_walletWithdrawId code: "WALLET_WITHDRAW" description: "Withdraw funds from a user's wallet account into their checking account." params: [ { name: "wallet", type: UUID, description: "User's wallet account." } { name: "checking" type: UUID description: "User's checking account." } { name: "amount" type: DECIMAL description: "Amount to withdraw from wallet." } { name: "effective", type: DATE, description: "Date of withdrawal." } ] transaction: { journalId: $journalIdExpression effective: "params.effective" description: "'Transfer ' + string(params.amount) + ' to checking account ' + string(params.checking) + ' from wallet ' + string(params.wallet)" } entries: [ { accountId: "params.wallet" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.checking" units: "params.amount" currency: "'USD'" description: "''" entryType: "'WALLET_DEPOSIT_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId code } } ``` **Response** ```json { "data": { "gl": { "journalId": "c2881874-007e-43e1-85ef-c263e8e361aa" }, "schema": { "createIndex": { "name": "TRANSACTION.BUDGET_CATEGORY", "on": "Transaction", "unique": false } }, "acct_set_bert": { "accountSetId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "name": "Bert" }, "acct_set_budgets": { "accountSetId": "e65e8d75-5f9f-4028-bb8c-5f8270a0d2a6", "name": "Budgets" }, "acct_set_ernie": { "accountSetId": "0a13f3b3-73dd-4a88-91be-ac0737bd7175", "name": "Ernie" }, "acct_set_settlement": { "accountSetId": "d46fcd5f-8a19-4909-8bf1-7915f5f91612", "name": "Settlement" }, "acct_set_users": { "accountSetId": "afa31512-022a-41cf-b223-a261f5526510", "name": "Users" }, "acct_set_wallets": { "accountSetId": "0e4d8596-6640-40d0-8a5c-5d9c8c10e534", "name": "Wallets" }, "acct_bert_budget": { "accountId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "code": "BERT.BUDGET", "name": "Bert's Budget" }, "acct_bert_cash": { "accountId": "74f4fbe3-daee-49c9-84ce-f5361b057a3d", "code": "BERT.CASH", "name": "Bert's Cash Acct." }, "acct_bert_checking": { "accountId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "code": "BERT.CHECKING", "name": "Bert's Checking Acct." }, "acct_bert_wallet": { "accountId": "7c1afcde-7863-41b8-9688-72730f4d61f9", "code": "BERT.WALLET", "name": "Bert's Wallet" }, "acct_budget_offset": { "accountId": "75da606c-03d6-4ea3-9a03-530710981f5b", "code": "BUDGET.OFFSET", "name": "Budget Offset" }, "acct_ernie_budget": { "accountId": "18b7c23f-460e-4593-937c-ccff0db3bcca", "code": "ERNIE.BUDGET", "name": "Ernie's Budget" }, "acct_ernie_checking": { "accountId": "9c424964-d102-4610-af8a-003ddd1bf270", "code": "ERNIE.CHECKING", "name": "Ernie's Checking Acct." }, "acct_ernie_credit_card": { "accountId": "55d75807-ec03-4d09-b607-472e9263985b", "code": "ERNIE.CREDIT_CARD", "name": "Ernie's Credit Card Acct." }, "acct_ernie_wallet": { "accountId": "a13bd92a-7450-46a5-adce-d5385805fd15", "code": "ERNIE.WALLET", "name": "Ernie's Wallet" }, "acct_settlement_card": { "accountId": "685fba2a-1ec6-4ae9-ace6-d9683d142c16", "code": "SETTLEMENT.CARD", "name": "Card Settlement" }, "acct_settlement_cash": { "accountId": "9583782b-3d02-45c0-a753-76e95710431d", "code": "SETTLEMENT.CASH", "name": "Cash Settlement" }, "acct_settlement_checking": { "accountId": "be381442-bd6f-4e52-a6dd-64380ffb1f45", "code": "SETTLEMENT.CHECKING", "name": "Checking Settlement" }, "tc_alloc_budget": { "tranCodeId": "2e92e3aa-9871-4c47-8c9a-5d76e6340769", "code": "ALLOC_BUDGET" }, "tc_dealloc_budget": { "tranCodeId": "d1eaa3c4-ea60-4da3-b225-cf2955391824", "code": "DEALLOC_BUDGET" }, "tc_assign_to_budget": { "tranCodeId": "95ede9f4-b3f6-4cc3-ab18-338fd9f41e8b", "code": "ASSIGN_TO_BUDGET" }, "tc_record_tx": { "tranCodeId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "code": "RECORD_TX" }, "tc_record_pending_tx": { "tranCodeId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "code": "RECORD_PENDING_TX" }, "tc_record_settle_pending_tx": { "tranCodeId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "code": "RECORD_SETTLE_PENDING_TX" }, "tc_wallet_transfer": { "tranCodeId": "62d8644c-ea6d-46b9-b295-98d006e5a116", "code": "WALLET_TRANSFER" }, "tc_wallet_deposit": { "tranCodeId": "8e924fb1-de38-47c4-86a8-ce93012c5711", "code": "WALLET_DEPOSIT" }, "tc_wallet_withdraw": { "tranCodeId": "499a549a-3da9-4256-a0dc-c59f69602df2", "code": "WALLET_WITHDRAW" } } } ``` **Variables** ```json { "journalId": "c2881874-007e-43e1-85ef-c263e8e361aa", "journalIdExpression": "uuid('c2881874-007e-43e1-85ef-c263e8e361aa')", "set_bertId": "65bc724c-6767-4f35-90f9-279a12f95fd4", "set_budgetsId": "e65e8d75-5f9f-4028-bb8c-5f8270a0d2a6", "set_ernieId": "0a13f3b3-73dd-4a88-91be-ac0737bd7175", "set_settlementId": "d46fcd5f-8a19-4909-8bf1-7915f5f91612", "set_usersId": "afa31512-022a-41cf-b223-a261f5526510", "set_walletsId": "0e4d8596-6640-40d0-8a5c-5d9c8c10e534", "bertBudgetId": "d7284018-0f6f-4a53-87b1-23f0d22f0883", "bertCashId": "74f4fbe3-daee-49c9-84ce-f5361b057a3d", "bertCheckingId": "f3d6f928-9bfe-4029-ad08-6473acf38465", "bertWalletId": "7c1afcde-7863-41b8-9688-72730f4d61f9", "budgetOffsetId": "75da606c-03d6-4ea3-9a03-530710981f5b", "budgetOffsetIdExpression": "uuid('75da606c-03d6-4ea3-9a03-530710981f5b')", "ernieBudgetId": "18b7c23f-460e-4593-937c-ccff0db3bcca", "ernieCheckingId": "9c424964-d102-4610-af8a-003ddd1bf270", "ernieCreditCardId": "55d75807-ec03-4d09-b607-472e9263985b", "ernieWalletId": "a13bd92a-7450-46a5-adce-d5385805fd15", "settlementCardId": "685fba2a-1ec6-4ae9-ace6-d9683d142c16", "settlementCashId": "9583782b-3d02-45c0-a753-76e95710431d", "settlementCheckingId": "be381442-bd6f-4e52-a6dd-64380ffb1f45", "tc_allocBudgetId": "2e92e3aa-9871-4c47-8c9a-5d76e6340769", "tc_deallocBudgetId": "d1eaa3c4-ea60-4da3-b225-cf2955391824", "tc_assignToBudgetId": "95ede9f4-b3f6-4cc3-ab18-338fd9f41e8b", "tc_recordTxId": "15a1b0c5-bad0-4ac1-ac0a-a1a078fc14ae", "tc_recordPendingTxId": "0b92fef4-7337-4d5d-9d6c-441da46cc34e", "tc_recordSettlePendingTxId": "673649ee-6aca-471a-8f55-86dc5cc4f5f2", "tc_walletTransferId": "62d8644c-ea6d-46b9-b295-98d006e5a116", "tc_walletDepositId": "8e924fb1-de38-47c4-86a8-ce93012c5711", "tc_walletWithdrawId": "499a549a-3da9-4256-a0dc-c59f69602df2" } ``` --- # Tutorials These tutorials cover the skills needed to work with the Twisp accounting core with simple, step-by-step instructions and relevant examples. ## Foundations - [Setting Up Accounts](https://www.twisp.com/docs/tutorials/setting-up-accounts.md): Create, modify, and delete accounts. - [Posting Transactions](https://www.twisp.com/docs/tutorials/posting-transactions.md): Use tran codes to write to the ledger. - [Pulling Balances](https://www.twisp.com/docs/tutorials/pulling-balances.md): Query balances for accounts and sets. - [Organizing with Account Sets](https://www.twisp.com/docs/tutorials/organizing-with-account-sets.md): Add structure to your accounts with sets. - [Restructuring a Chart of Accounts](https://www.twisp.com/docs/tutorials/restructuring-a-chart-of-accounts.md): Safely delete, empty, and detach sets to reorganize a live hierarchy. - [Building Tran Codes](https://www.twisp.com/docs/tutorials/building-tran-codes.md): Design transaction codes for your ledger. - [Working with Journals](https://www.twisp.com/docs/tutorials/working-with-journals.md): Configure a multi-journal ledger. - [Utilizing Indexes](https://www.twisp.com/docs/tutorials/indexes.md): Add indexes to query data. - [Creating Calculations](https://www.twisp.com/docs/tutorials/calculations.md): Custom balances on additional dimensions. - [Enforcing Velocity](https://www.twisp.com/docs/tutorials/velocity.md): Enforce velocity limits on accounts. ## Twisp 101 This tutorial walks through building an example project which offers checking, savings, and loan products. Start the tutorial: [Twisp 101](https://www.twisp.com/docs/tutorials/twisp-101.md). --- # Creating Custom Indexes Learn how to create and use custom indexes in Twisp to efficiently query records based on specific fields, including data within the metadata object. Custom indexes allow you to define specific ways to query your ledger data, enabling efficient filtering and sorting based on fields like `accountId`, `status`, or even nested values within the `metadata` object. By the end of this tutorial, you will be able to create a custom index tailored to your querying needs and use it to retrieve records efficiently. > **Task:** > > - Design a custom index with partition and sort keys using CEL. > - Create the index using the `schema.createIndex` mutation. > - Query records efficiently using the custom index. --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Designing a Custom Index Before creating an index, you need to decide how you want to query your data. Let's design an index on the `Account` table to quickly find accounts based on their `accountId` and a `category` field within their `metadata`. Key components of an index definition: * **`on`:** The record type (table) the index applies to (e.g., `Account`). * **`name`:** A unique, human-readable name for the index (e.g., `account_metadata_category`). * **`partition`:** How data is grouped. Queries *must* filter on the partition key(s). Good partitioning spreads data evenly. We'll partition by `accountId`. * `alias`: A name for the key (e.g., `accountId`). * `value`: A [CEL expression](https://twisp.com/docs/reference/cel) referencing the `document` (the record being indexed) to get the partition value (e.g., `document.accountId`). * **`sort` (Range Key):** Defines the order *within* a partition, enabling range queries (`gte`, `lte`) and sorting. We'll sort by `metadata.category`. * `alias`: A name for the key (e.g., `category`). * `value`: A CEL expression for the sort value (e.g., `string(document.metadata.category)` - casting to string ensures predictable sorting). * `sort`: `ASC` or `DESC`. * **`constraints` (Optional):** CEL expressions that must *all* be true for a record to be included. We'll only index accounts that *have* a `metadata.category`. * Example: `{ hasCategory: "has(document.metadata.category)" }` * **`unique` (Optional):** If `true`, ensures the combination of partition and sort keys is unique for each indexed record. Defaults to `false`. For our example: * **Name:** `account_metadata_category` * **On:** `Account` * **Partition:** `alias: "accountId"`, `value: "document.account_id"` * **Sort:** `alias: "category"`, `value: "string(document.metadata.category)"`, `sort: ASC` * **Constraints:** `{ hasCategory: "has(document.metadata.category)" }` * **Unique:** `false` ## Create the Custom Index Use the `schema.createIndex` mutation to create the index defined above. ```graphql mutation CreateAccountMetadataIndex { schema { createIndex( input: { name: "account_metadata_category" on: Account unique: false partition: [ { alias: "accountId", value: "document.account_id" } ] sort: [ { alias: "category" value: "string(document.metadata.category)" sort: ASC } ] constraints: { hasCategory: "has(document.metadata.category)" } } ) { name on unique partition { alias value } range { # 'range' is the field for sort keys in the response alias value sort } constraints historical search } } } ``` ```json { "data": { "schema": { "createIndex": { "name": "account_metadata_category", "on": "Account", "unique": false, "partition": [ { "alias": "accountId", "value": "document.account_id" } ], "range": [ { "alias": "category", "value": "string(document.metadata.category)", "sort": "ASC" } ], "constraints": { "hasCategory": "has(document.metadata.category)" }, "historical": false, "search": false } } } } ``` This mutation creates the `account_metadata_category` index on the `Account` table. The response confirms the index structure, including its partition and range (sort) keys. > **Note:** > > Twisp also supports `createHistoricalIndex` to index every version of a record and `createSearchIndex` for eventually consistent full-text search capabilities. ## Query Using the Custom Index To use your new index, specify `index: { name: CUSTOM }` in your query and provide the index name and filters in the `where.custom` argument. You **must** provide an equality (`eq`) filter for all partition key aliases. You can optionally provide filters (`eq`, `gte`, `lte`, `prefix`, etc.) for sort key aliases. ```graphql query QueryUsingCustomIndex { accounts( index: { name: CUSTOM } where: { custom: { index: "account_metadata_category" # Name of the custom index partition: [ { alias: "accountId" # Partition key alias value: { eq: "a1b2c3d4-e5f6-7890-1234-567890abcdef" } } ] sort: [ { alias: "category" # Sort key alias value: { eq: "Premium" } # Filter condition on sort key } ] } } first: 10 ) { nodes { accountId name metadata } pageInfo { hasNextPage endCursor } } } ``` ```json { "data": { "accounts": { "nodes": [ { "accountId": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "name": "Premium Customer Account", "metadata": { "category": "Premium", "region": "US-West" } } // ... other matching accounts up to 10 ], "pageInfo": { "hasNextPage": false, "endCursor": "..." } } } } ``` This query efficiently retrieves `Account` records using the `account_metadata_category` index, filtering first by the `accountId` partition and then by the `category` sort key. ## Conclusion In this tutorial, you learned how to design and create a custom index using the `schema.createIndex` mutation. You saw how to define partition keys, sort keys, and constraints using CEL expressions. Finally, you learned how to leverage your custom index in queries for efficient data retrieval based on specific fields, including nested metadata values. Custom indexes are a powerful tool for optimizing query performance in Twisp. Consider creating them for your common query patterns. --- # Organizing with Account Sets In this tutorial, we'll explore how to use account sets to organize your chart of accounts. With the structure provided by account sets, you can enhance your ledger with custom materialized balances and organize accounts into groups based on their purpose or function. > **Task:** > > - Create new sets with the `createAccountSet` mutation > - Add members to a set with the `addToAccountSet` mutation > - Get set data and its members with the `accountSet` query > - Update fields on a set with the `updateAccountSet` mutation > - Delete a set with the `deleteAccountSet` mutation --- ## Prerequisites Before beginning this tutorial, you should have a basic understanding of Twisp's ledger system and how transactions, accounts, and entries work. Review the [Accounting Core](https://www.twisp.com/docs/accounting-core.md) docs for more context. If you'd like to follow along with the steps in this tutorial, you should have added accounts to your ledger. See the tutorial on [Setting Up Accounts](https://www.twisp.com/docs/tutorials/setting-up-accounts.md). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Create an account set To create a new [AccountSet](https://www.twisp.com/docs/reference/graphql/types/object.md#account-set), we'll use the `createAccountSet` mutation. This mutation takes several arguments: - `accountSetId`: A unique identifier for the account set. - `journalId`: The ID of the journal to which the account set belongs. - `name`: The name of the account set. - `description`: A description of the account set. - `normalBalanceType`: The normal balance to use for rolling up balances for this account set (either `DEBIT` or `CREDIT`). Let's create an account set to hold customer's accounts. We'll call it `"Customers"` and set the `normalBalanceType` to `CREDIT`: **Request** ```graphql mutation CreateAccountSet($accountSetCustomersId: UUID!, $journalGLId: UUID!) { createAccountSet( input: { accountSetId: $accountSetCustomersId journalId: $journalGLId name: "Customers" description: "All customer wallets." normalBalanceType: DEBIT } ) { accountSetId name description code } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets.", "code": "Ke8_GJexQNmYUifxYHtsqIIstZ_OUUg3g5Eq87el_FE" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` This operation will add a new account set to the ledger. Note the `DEBIT` balance type indicates that this account set is of the type that has a debit normal balance, which means that debits increase the balance and credits decrease the balance. ## Add set members To add an account to an account set, use the mutation `addToAccountSet`. The mutation takes two arguments: - `id`: Unique identifier for the account set to which the member will be added. - `member`: An `AccountSetMemberInput` object containing the unique identifier of the account or account set to be added as a member, as well as the type of member (`ACCOUNT` or `ACCOUNT_SET`). **Request** ```graphql mutation AddToAccountSet( $accountSetCustomersId: UUID! $accountCustomerAliciaId: UUID! ) { addToAccountSet( id: $accountSetCustomersId member: { memberId: $accountCustomerAliciaId, memberType: ACCOUNT } ) { accountSetId members(first: 10) { nodes { ... on Account { accountId name code } } } } } ``` **Response** ```json { "data": { "addToAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "members": { "nodes": [ { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "name": "Alicia", "code": "CUST.Alicia" } ] } } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c" } ``` The `addToAccountSet` field returns the updated account set, including its ID and the list of members. In this case, the list of members is limited to the first 10 nodes, and only the `accountId`, `name`, and `code` fields are included in the response. > **Task:** > > Try creating another account for a customer named "Bobby", then add their account to the "Customers" account set. ## Nest account sets within other sets One powerful feature of [AccountSets](https://www.twisp.com/docs/reference/graphql/types/object.md#account-set) is that they can be nested within other sets. This allows us to create more complex structures for our chart of accounts. To nest one AccountSet within another, it's as simple as use the same `addToAccountSet` mutation, but with a `memberType` of `ACCOUNT_SET`. For example: ```graphql mutation AddToAccountSetNested( addToAccountSet( id: "" member: { memberId: " **Note:** > > A sub-set can only be nested (or later re-nested) while it has never been populated — that is, while it is empty and has never contained a member. Once a set has ever held a member, it is structurally frozen and cannot be attached to or detached from a parent. To restructure a hierarchy that is already in use, create a new set, move the members into it, then soft-delete the old set. By adding nested sets, you can create tree-like structures. Can you recreate this tree using the commands you've learned so far? ```mermaid graph BT cust[/Customers\] inac[/Inactive customers\] alicia[Alicia] bobby[Bobby] cal[Cal] alicia & bobby & inac --> cust cal --> inac ``` ## Query members of an account set To query the members, we'll use the `accountSet` query and request the `members` field of the "Customers" set created earlier. **Request** ```graphql query GetAccountSet { accountSet(id: "29ef3f18-97b1-40d9-9852-27f1607b6ca8") { accountSetId name description members(first: 10) { nodes { ... on Account { accountId code name } } } } } ``` **Response** ```json { "data": { "accountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customers", "description": "All customer wallets.", "members": { "nodes": [ { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "code": "CUST.Bobby", "name": "Bobby" }, { "accountId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "code": "CUST.Alicia", "name": "Alicia" } ] } } } } ``` Note that the `members` field returns a paginated response. Because account sets can contain accounts _or_ other account sets, we can use an [inline fragment](https://graphql.org/learn/queries/#inline-fragments) to specify which fields are to be returned depending on the type. The "Customers" set only contains accounts at this point, so no fields for account sets need to be specified. > **Note:** > > If you are unfamiliar with union types in GraphQL, you can find a good summary on the official docs: [https://graphql.org/learn/schema/#union-types](https://graphql.org/learn/schema/#union-types). ## Update fields on an account set The `updateAccountSet` mutation is used to update fields of an existing account set (name, description, metadata, etc.). It takes as input the `id` of the account set to be updated and an `input` object containing the fields to update. **Request** ```graphql mutation UpdateAccountSet($accountSetCustomersId: UUID!) { updateAccountSet( id: $accountSetCustomersId input: { name: "Customer Wallets", code: "CUSTOMERS.WALLETS" } ) { accountSetId name code history(first: 2) { nodes { version name code } } } } ``` **Response** ```json { "data": { "updateAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8", "name": "Customer Wallets", "code": "CUSTOMERS.WALLETS", "history": { "nodes": [ { "version": 3, "name": "Customer Wallets", "code": "CUSTOMERS.WALLETS" }, { "version": 2, "name": "Customers", "code": "Ke8_GJexQNmYUifxYHtsqIIstZ_OUUg3g5Eq87el_FE" } ] } } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` This mutation can be useful when there is a need to update the name of an existing account set due to changes in an organization's structure or operations. The response from the mutation can be used to verify that the update was successful and to track changes to the account set over time. ## Delete an account set The `deleteAccountSet` mutation performs a mark-only soft delete of an existing account set. It takes as input the `id` of the account set to be deleted. Rather than removing anything, it marks the set deleted; the set must be empty first (remove its members before deleting), and the delete does not remove members or alter the membership graph. There is no un-delete. **Request** ```graphql mutation DeleteAccountSet($accountSetCustomersId: UUID!) { deleteAccountSet(id: $accountSetCustomersId) { accountSetId } } ``` **Response** ```json { "data": { "deleteAccountSet": { "accountSetId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } } } ``` **Variables** ```json { "accountSetCustomersId": "29ef3f18-97b1-40d9-9852-27f1607b6ca8" } ``` This mutation can be useful when an account set is no longer needed or was created in error. If the set still has members, empty it first, then delete it. ## Conclusion In this tutorial, we've explored how to use [AccountSets](https://www.twisp.com/docs/reference/graphql/types/object.md#account-set) to organize accounts. We've covered how to create an account set, add members to it, nest sets within other sets, query set members, update fields of a set, and delete a set. By using account sets to organize your chart of accounts, you can create more flexible and powerful structures that better fit the needs of your business or organization. --- # Posting Transactions In this tutorial, we will learn how to post transactions using the GraphQL API. > **Task:** > > - Determine a tran code to use and understand its `params` > - Post a transaction with a specified tran code using the `postTransaction` mutation > - Query transactions to inspect entries written with the `transaction` query Posting transactions in Twisp is a simple process that involves specifying the transaction details and the transaction code (tran code) to use. Transactions are written to the ledger, and entries are created for as specified by the tran code used. --- ## Prerequisites Before you start, you should have added accounts to your ledger. See the tutorial on [Setting Up Accounts](https://www.twisp.com/docs/tutorials/setting-up-accounts.md). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Choose a transaction code You will need to choose a transaction code to use before posting transactions. Transaction codes are pre-defined templates that specify how transactions should be processed. Transaction codes define the accounts and amounts to debit and credit, the currency to use, and any other parameters required by the transaction. Transaction codes are usually created during the configuration of a Twisp ledger and can be customized to meet the specific needs of your organization. For this tutorial, we will use a `BOOK_TRANSFER` transaction code. The `BOOK_TRANSFER` transaction code requires the following parameters: - the account to _debit_ - the account to _credit_ - the _amount_ to transfer - the _currency_ of the transfer - the _effective date_ of the transfer transaction For a more in-depth look at this tran code, review the tutorial page on [Building Tran Codes](https://www.twisp.com/docs/tutorials/building-tran-codes.md). Once a transaction code has been selected, a transaction can be written using the `postTransaction` mutation. ## Write a `postTransaction` mutation To post a transaction, you will need to provide the following inputs: - `transactionId`: a unique identifier for the transaction - `tranCode`: the transaction code to use - `params`: a set of key-value parameters as specified by the transaction code Once the transaction is posted, Twisp will write the transaction to the ledger and create entries for each account affected by the transaction. ### Provide a unique `transactionId` It's important to provide a unique `transactionId` when posting a transaction. This can help prevent duplicate transactions from being posted accidentally, i.e. it ensures idempotent transactions. ### Specify values for all `params` Make sure to specify all required values in the `params` object for the transaction code you are using. Here's an example mutation showing how to post a transaction with the `BOOK_TRANSFER` tran code: **Request** ```graphql mutation PostTransaction( $accountCustomerAliciaId: UUID! $accountCustomerBobbyId: UUID! ) { postTransaction( input: { transactionId: "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" tranCode: "BOOK_TRANSFER" params: { crAccount: $accountCustomerAliciaId drAccount: $accountCustomerBobbyId amount: "1.00" currency: "USD" effective: "2022-09-08" } } ) { transactionId } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "6b5e47b6-60d2-49bf-8210-0e4c3dd3ec68" } } } ``` **Variables** ```json { "accountCustomerAliciaId": "260fd651-8819-4f99-9c8a-87d27e03ee4c", "accountCustomerBobbyId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" } ``` This code example shows a transaction that transfers funds between two accounts. The `params` object specifies all required values for this transaction, including the customer account to credit (`crAccount`) and customer account to debit (`drAccount`), the amount to transfer (`amount`), the currency (`currency`), and the effective date of the transaction (`effective`). Keep in mind that transactions are processed according to the rules defined in the transaction code. It is important to ensure that the transaction code and its associated parameters are correct before posting the transaction. ## Query transactions to see entries written Any posted transaction can be queried to view its fields and the entries that were written to the ledger. Here's an example query: **Request** ```graphql query GetTransaction { transaction(id: "434696a7-56e5-4e14-a97a-884820690a22") { description effective tranCode { code } journal { name } metadata entries(first: 10) { nodes { sequence entryType layer direction account { name } amount { formatted(as: { locale: "en-US" }) } } } } } ``` **Response** ```json { "data": { "transaction": { "description": "Exchange 2.37 USD for EUR at a 1:0.85 exchange rate.", "effective": "2022-09-11", "tranCode": { "code": "EXCHANGE_CURRENCY" }, "journal": { "name": "GL" }, "metadata": { "buy": "EUR", "rate": "0.85", "sell": "USD" }, "entries": { "nodes": [ { "sequence": 0, "entryType": "FX_SELL_DR", "layer": "SETTLED", "direction": "CREDIT", "account": { "name": "Alicia" }, "amount": { "formatted": "$2.37" } }, { "sequence": 1, "entryType": "FX_SELL_CR", "layer": "SETTLED", "direction": "DEBIT", "account": { "name": "Foreign Exchange Broker" }, "amount": { "formatted": "$2.37" } }, { "sequence": 2, "entryType": "FX_BUY_DR", "layer": "SETTLED", "direction": "CREDIT", "account": { "name": "Foreign Exchange Broker" }, "amount": { "formatted": "€2.0145" } }, { "sequence": 3, "entryType": "FX_BUY_CR", "layer": "SETTLED", "direction": "DEBIT", "account": { "name": "Alicia" }, "amount": { "formatted": "€2.0145" } } ] } } } } ``` This GraphQL query returns a transaction with its description, effective date, transaction code, journal name, metadata, and entries. The entries include the sequence, entry type, layer, direction, account name, and formatted amount. ## Conclusion Posting transactions in Twisp is a straightforward process that requires specifying the transaction code to use along with the values for all parameters as defined by the tran code. The `postTransaction` mutation will return the posted transaction. Transactions can also be queried using the `transaction` and `transactions` queries. --- # Pulling Balances In this tutorial, we will learn how to query account balances using the GraphQL API. Twisp provides a robust GraphQL schema that allows users to query account balances using a variety of filters and indexes. > **Task:** > > - Get a single balance for a specific account and currency with the `balance` query > - Get a set of balances for a given set of conditions with the `balances` query > - Pull the balance for an account with the `Account.balance` field > - Pull an aggregate balance for accounts in a set with the `AccountSet.balance` field --- ## Prerequisites Before you start, you should have added accounts to your ledger and posted some transactions. See the tutorials on [Setting Up Accounts](https://www.twisp.com/docs/tutorials/setting-up-accounts.md) and [Posting Transactions](https://www.twisp.com/docs/tutorials/posting-transactions.md). ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ### Retrieve an account UUID First, you will need to retrieve the UUID for the account that you want to query. You can do this by querying for the account using the `accounts` query, and specifying any relevant filters. For example, to retrieve the UUID for an account with the code `CHECKING`, you can use the following GraphQL query: ```graphql query { accounts( index: { name: CODE } where: { code: { eq: "CHECKING" } } first: 1 ) { nodes { accountId name } } } ``` This query will return the UUID and name for any accounts with the code `CHECKING`. ## Query the account balance Once you have the UUID for the account that you want to query, you can use the `balance` query to retrieve the balance for that account. The `balance` query takes two arguments: - `journalId` (required): the UUID of the journal to retrieve the balance from. - `currency` (required): the currency code of the balance to retrieve. For example, to retrieve the settled normal balance for the account with UUID `"3ea12e45-7df2-4293-9434-feb792affc91"` in journal with UUID `"4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b"`, you can use the following GraphQL query: ```graphql query { balance( accountId: "3ea12e45-7df2-4293-9434-feb792affc91" journalId: "4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b" currency: "USD" ) { settled { normalBalance { units } } } } ``` This query will return the USD settled normal balance amount in decimal units for for the account given in the specified journal. ## Query multiple balances If you want to retrieve multiple balances at once, you can use the `balances` query instead. The `balances` query takes the same arguments as the `balance` query, but also allows you to specify additional filters to narrow down the results. For example, to retrieve all balances for the account with UUID `"3ea12e45-7df2-4293-9434-feb792affc91"` in currency `"USD"`, you can use the following GraphQL query: ```graphql query { balances( index: { name: ACCOUNT_ID } where: { accountId: { eq: "3ea12e45-7df2-4293-9434-feb792affc91" } } first: 5 ) { nodes { journalId currency settled { normalBalance { units } } } } } ``` This query retrieves information about the balances of a specific account. It requests the first five balances for the account with an `accountId` of `"3ea12e45-7df2-4293-9434-feb792affc91"`, and returns the `journalId`, `currency`, and `normalBalance` for each balance on the account. ## Query a balance using the `Account.balance` field To query an account balance using the `balance` field on an `Account` type, you will need to provide the `journalId` and `currency` arguments. The `journalId` argument specifies the ID of the journal for the balance, and the `currency` argument specifies the currency of the balance. For example, to retrieve the USD settled normal balance for the account with UUID `"3ea12e45-7df2-4293-9434-feb792affc91"` in journal with UUID `"4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b"`, you can use the following GraphQL query: ```graphql query { account(id: "3ea12e45-7df2-4293-9434-feb792affc91") { balance(journalId: "4e9d9f6c-0cc3-4a8e-9d1a-0de5a47b0c8b", currency: "USD") { settled { normalBalance { units } } } } } ``` Note that this is effectively the same as using the `balance` query above: it will return the account's USD settled normal balance amount in decimal units in the specified journal. > **Note:** > > You can also query for multiple balances on accounts using the `Account.balances` field. This can be useful for accounts that contain entries across multiple journals, or for accounts that contain multiple currencies. ## Query a balance for an account set To query balances for an account set, you can use the `balances` query with the `accountSetId` argument. Balances for an account set are calculated by summing the journal balances of all accounts that are members of the account set. In this context, the journal balance for an account is the balance for all entries posted to the journal that the account set is tied to. So an account set will only show balances for entries posted to the account set's journal. For example, to retrieve the USD balance for an account set with UUID `"d1a2e7e9-7a6d-4d82-bf6c-2c8d91b1c1f6"`, you can use the following GraphQL query: ```graphql query { accountSet(id: "d1a2e7e9-7a6d-4d82-bf6c-2c8d91b1c1f6") { balance(currency: "USD") { settled { normalBalance { units } } } } } ``` This query retrieves the USD balance of the account set with UUID `"d1a2e7e9-7a6d-4d82-bf6c-2c8d91b1c1f6"`. The `currency` argument in the balance field specifies the currency to use for the balance calculation. The query includes the `normalBalance` field of the `settled` balance layer, which returns the decimal units for the normal balance of the account set. > **Note:** > > You can also query for multiple balances on account sets using the `AccountSet.balances` field. This can be useful for account sets that track accounts across multiple currencies. ## Conclusion Twisp's GraphQL schema provides a powerful set of tools for querying account balances. By leveraging the `balance` and `balances` queries, you can quickly and easily retrieve the balance information you need for your ledger entries and financial reporting. --- # Restructuring a Chart of Accounts How to safely delete, empty, and detach account sets when reorganizing a live chart of accounts — and why the ledger enforces the order it does. A chart of accounts is rarely static. Businesses reorganize: a product line is wound down, two departments merge, a rollup is split in two. This guide shows how to tear down and reshape an existing account-set hierarchy **without ever producing a wrong balance**, and explains the rules the ledger enforces so the reorganization is safe even while transactions are being posted concurrently. > **Task:** > > - Understand why account sets are **append-structured** while in use > - Soft-delete a set with the `deleteAccountSet` mutation > - Empty a set with `removeFromAccountSet` > - Detach a deleted, empty set from its parents > - Tear down a multi-level hierarchy bottom-up --- ## Prerequisites You should already be comfortable creating account sets and adding members to them. If not, start with [Organizing With Account Sets](https://www.twisp.com/docs/tutorials/organizing-with-account-sets.md). For background on how account sets roll balances up a hierarchy, see [Chart Of Accounts](https://www.twisp.com/docs/accounting-core/chart-of-accounts.md). --- ## The one rule that shapes everything An account set's balance is the rolled-up sum of every account reachable through its members. The ledger computes those rollups continuously and concurrently with transaction posting. That concurrency is the reason for the single rule that governs restructuring: > **A set's structure is append-only while the set is in use.** Once a set has > ever held a member, it is *frozen*: it cannot be moved to a different parent, > and it cannot be detached from its current parents — while it is active. Concretely, `has_members` on a set is a one-way latch. It flips from false to true the first time you add any member, and it never flips back — not even after you remove every member. A set that has *never* been populated is still free to attach and detach; a set that has *ever* been populated is structurally fixed. This is not an arbitrary restriction. Moving or detaching a populated set while transactions are in flight is precisely the operation that can strand a balance in the wrong place — a posting that resolved the old structure commits its contribution through an edge you just removed, and the parent is left holding money with no path to explain it. Rather than take heavyweight locks on every affected rollup, the ledger makes the structural question *append-only*, which is the shape it can answer correctly without locks. So you never *move* a populated set. You **tear it down and rebuild**. The rest of this guide is the safe teardown sequence. --- ## Step 1 — Soft-delete the set Deleting an account set is a **mark-only, soft delete**. It always succeeds, and it does exactly one thing: it marks the set as `DELETED`. ```graphql mutation { deleteAccountSet(id: "acct-set-to-remove") { accountSetId status } } ``` What `deleteAccountSet` does **not** do is just as important: - It does **not** remove the set's members. - It does **not** detach the set from its parents. - It does **not** change any balance. A deleted set that still sits under a parent keeps contributing to that parent until you remove its members. What the mark *does* do is close the set off from further structural growth, immediately: - A deleted set can **no longer be added** to any other set. - A deleted set can **no longer accept new members**. - The mark is **permanent** — there is no un-delete. Think of the delete as "retire this set." It stops the set from being reused or extended, which is exactly what makes the later detach safe (see [Why the order matters](https://www.twisp.com/docs/tutorials/restructuring-a-chart-of-accounts.md#why-the-order-matters)). --- ## Step 2 — Empty the set A set contributes to its parents only through the accounts reachable beneath it. To stop that contribution, remove the members. Account members can be removed at any time, including from a deleted set: ```graphql mutation { removeFromAccountSet( accountSetId: "acct-set-to-remove" memberId: "account-123" memberType: ACCOUNT ) { accountSetId } } ``` Remove members until the set is physically empty. Each removal is balance-exact: the account's contribution is reversed out of the set and every ancestor at the moment you remove it. If the set contains **sub-sets**, empty and tear those down first — teardown is bottom-up (see [Step 4](https://www.twisp.com/docs/tutorials/restructuring-a-chart-of-accounts.md#step-4-tearing-down-a-multi-level-hierarchy)). --- ## Step 3 — Detach the empty, deleted set from its parents Once a set is **deleted** and **physically empty**, it may be removed from each parent it belongs to: ```graphql mutation { removeFromAccountSet( accountSetId: "parent-set" memberId: "acct-set-to-remove" memberType: ACCOUNT_SET ) { accountSetId } } ``` There is a short **drain window** to be aware of. Immediately after you delete a set, transactions that were already in flight might still be resolving it as a live member. The ledger waits until those have provably finished before it lets you detach — the same drain window used elsewhere in the ledger, on the order of a minute. If you attempt the detach too soon, it does not fail permanently. You get a **retriable** error carrying a `retryAfter` timestamp — the exact instant the drain window closes: ```json { "errors": [ { "message": "deleted account set ... is still within its drain horizon; retry after it clears", "extensions": { "retriableError": true, "retryAfter": "2026-07-08T18:42:10Z" } } ] } ``` Wait until `retryAfter` and retry; the detach then succeeds. If instead you get a non-retriable error saying the set **still has members**, it is not yet empty — return to [Step 2](https://www.twisp.com/docs/tutorials/restructuring-a-chart-of-accounts.md#step-2-empty-the-set). --- ## Step 4 — Tearing down a multi-level hierarchy To dismantle a whole subtree, work **bottom-up**. For each set, from the leaves toward the root: 1. **Delete** it (mark it `DELETED`). 2. **Empty** it — remove account members, and detach any child sets that are themselves already deleted, empty, and drained. 3. **Detach** it from its parents (waiting out the drain window if needed). Doing this leaf-first guarantees that whenever you detach a set, it is already empty: its children were emptied and detached in earlier rounds, so nothing is left to strand in the parent. To **reshape** rather than fully remove a hierarchy — for example, to re-home a populated sub-set under a different parent — the pattern is **create-new, not move**: 1. Create the new set in its new location and add the members there. 2. Tear down the old set with the delete → empty → detach sequence above. Because a populated set is frozen, "move" is always expressed as "build the new shape, then retire the old one." --- ## Why the order matters The delete → empty → detach order is not bureaucracy; each step removes a specific way the reorganization could otherwise corrupt a balance: - **Delete first** closes the set to new members and new parents. This bounds the set of transactions that could still be posting into it to a finite, shrinking population — the ones already in flight — with no new entrants. - **Empty next** drives the set's contribution to zero, so detaching it reverses nothing. - **The drain window** before detach waits out every in-flight transaction that could still be resolving the (now deleted) set as live. Once they have all committed, no straggler can seed a contribution through the edge you are about to remove. With all three in place, detaching a deleted, empty, drained set is a pure structural edge removal that touches no balance — safe to do even under a live posting workload. Skip any one of them and you reintroduce exactly the race the append-only rule exists to prevent. --- ## Further reading - [Organizing With Account Sets](https://www.twisp.com/docs/tutorials/organizing-with-account-sets.md) — building hierarchies in the first place. - [Account Sets](https://www.twisp.com/docs/reference/ledger/account-sets.md) — the full account-set operation reference. - [Chart Of Accounts](https://www.twisp.com/docs/accounting-core/chart-of-accounts.md) — how and why Twisp models a chart of accounts with sets. --- # Setting Up Accounts In this tutorial, we will learn how to manage accounts using the GraphQL API. Managing financial accounts is a crucial aspect of many applications. By the end of this tutorial, you will have a solid understanding of how to manage accounts using the Twisp GraphQL API and be ready to incorporate this functionality into your own applications. > **Task:** > > - Create a new account using the `createAccount` mutation > - Update fields on an existing account using the `updateAccount` mutation > - Delete (lock) an account using the `deleteAccount` mutation --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Create an account The `createAccount` mutation is used to create new accounts with the given `accountId`, `name`, `code`, `description`, `normalBalanceType`, and `status`. The `accountId` is a unique identifier for the account that is being created, and it is required as an input field for this mutation. The `name` input field specifies the name for the new account that is being created. The `code` input field is a shorthand code for the account, which is often an abbreviated version of the account name. The `description` input field is a brief explanation of the account that is being created. The `status` input field represents the current status for the new account that is being created. This field specifies whether the account is active or closed (locked). By default, all accounts are `ACTIVE`. When an account is `LOCKED`, it cannot be updated unless you are also changing its status back to `ACTIVE`. Any attempt to write a ledger entry to a locked account will raise an error. **Request** ```graphql mutation CreateAccount($accountCardSettlementId: UUID!) { createAccount( input: { accountId: $accountCardSettlementId name: "Card Settlement" code: "SETTLE.CARD" description: "Settlement account for card transactions." normalBalanceType: CREDIT status: ACTIVE } ) { accountId name code description normalBalanceType } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "name": "Card Settlement", "code": "SETTLE.CARD", "description": "Settlement account for card transactions.", "normalBalanceType": "CREDIT" } } } ``` **Variables** ```json { "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` This `createAccount` operation returns the newly created account's `accountId`, `name`, `code`, `description`, and `normalBalanceType`. ## Update an account To update an existing account, we can use the `updateAccount` mutation. We need to provide the `id` of the account we want to update, as well as the fields we want to update. Here's an example mutation: **Request** ```graphql mutation UpdateAccount($accountCardSettlementId: UUID!) { updateAccount(id: $accountCardSettlementId, input: { code: "CARD.SETTLE" }) { accountId code history(first: 2) { nodes { version code } } } } ``` **Response** ```json { "data": { "updateAccount": { "accountId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8", "code": "CARD.SETTLE", "history": { "nodes": [ { "version": 2, "code": "CARD.SETTLE" }, { "version": 1, "code": "SETTLE.CARD" } ] } } } } ``` **Variables** ```json { "accountCardSettlementId": "a9c8dde6-c0e5-407c-9d99-029c523f7ea8" } ``` This is a GraphQL mutation operation that allows the user to update an account's code. The mutation is called `updateAccount` and takes in two arguments: `id` and `input`. The `id` argument is a unique identifier for the account being updated, and the `input` argument contains the updates to apply to the account. In this specific example, the `id` argument is passed in with the `$accountCardSettlementId` variable. The response from this mutation includes several fields. The `accountId` field returns the unique identifier for the updated account, while the `code` field returns the updated code for the account. Additionally, the `history` field returns the two most recent versions of the account, along with their corresponding `version` and `code`. This allows the user to track the changes made to the account over time. Overall, this mutation provides a flexible and powerful way to update accounts in the Twisp system, and the response includes valuable information that can be used to track changes and ensure data integrity. ## Delete an account Because Twisp is an immutable database, we cannot fully "delete" an account. Deleting an account in Twisp instead marks its status as `LOCKED`, meaning that no entries can be posted to the account. A locked account can only be updated to change its status back to `ACTIVE`. To delete (lock) an account, we can use the `deleteAccount` mutation. We need to provide the `id` of the account we want to delete. Here's an example mutation: **Request** ```graphql mutation DeleteAccount($accountCustomerBobbyId: UUID!) { deleteAccount(id: $accountCustomerBobbyId) { accountId status } } ``` **Response** ```json { "data": { "deleteAccount": { "accountId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b", "status": "LOCKED" } } } ``` **Variables** ```json { "accountCustomerBobbyId": "ae9d36cb-dcf5-41a9-bc1e-99a1cf56ef5b" } ``` Once the account is deleted, this mutation returns two fields: `accountId` and `status`. `accountId` is a UUID that uniquely identifies the deleted account, while `status` is the current status of the account. This mutation is useful if you need to remove an account that is no longer needed or was created in error. By deleting the account, you can ensure that it is no longer used in any future transactions or reports. ## Conclusion In this tutorial, we explored how to create, update, and delete accounts using the Twisp GraphQL API. We saw how to use the `createAccount`, `updateAccount`, and `deleteAccount` mutations to perform these operations. With this knowledge, you can now begin building your own applications that use the Twisp API to manage financial accounts. --- # Twisp 101 Tutorial Work through the steps in this tutorial to learn how to start building on the Twisp accounting core. Welcome to the Twisp 101 Tutorial! ## Context This tutorial is designed to walk you through the foundational features of the Twisp Accounting Core. The example project we'll be using is an imaginary neobank called "Zuzu" which offers checking, savings, and loan products to under-banked customers in the US. We'll work through the steps needed to **set up a chart of accounts**, orchestrate the types of financial activity by **defining transaction codes**, and **write some ledger entries** by posting transactions. Most new Twisp customers will have a similar process regardless of the specifics of their product. The fundamental stages are: 1. **Design** the elements which give a structure and meaning to the accounting system. 2. **Test** the design to ensure that it satisfies working requirements. 3. **Deploy** products backed by the Twisp core. 4. **Monitor** for performance, usage, auditing, and reporting. In this tutorial, we'll focus on the Design and Test stages. ## The Project _Zuzu_ is a new neobank offering personal banking services: checking, savings, and loans. They work with multiple partner banks, issue debit cards, and provide online and mobile banking applications. Building on Twisp's accounting core, Zuzu has the following project requirements. - A chart of accounts to model both internal company accounts as well as customer accounts, including: - Checking accounts - Savings accounts - Loans issued - A double-entry ledger for recording financial activity, including common transactions like: - Deposits & withdrawals - Direct transfers between customers - Fees charged for a variety of reasons In the interest of getting something working, we'll start simple and just implement the accounting components needed to support the **checking** product and its primary transactions. ## Step 0: Establish API Connection Twisp is an API-first product. Before we can do anything, we need to connect to the API. ### Connect to GraphQL API When you are invited to Twisp, you will receive an account to access the Twisp Console. The console provides a [GraphiQL](https://github.com/graphql/graphiql) interface for interacting with the GraphQL API for your provisioned accounting core. This is the quickest and easiest way to get connected. Once you have proper auth credentials, you can connect to the GraphQL API from any client. ### Inspect the Schema The GraphiQL interface provides built-in docs, so you can explore the schema and read documentation about queries, mutations, and types. Let's confirm that you're connection is working. Run an introspection query directly: **Introspection** ```graphql query { __schema { types { name kind description } } } ``` You should get a large response with all the various types like `Account`, `Entry`, etc. --- # Step 1: Create a Primary Journal Before we begin designing accounts, we need to define a journal within which to organize transactions. In most cases, a single [Journal](https://www.twisp.com/docs/reference/graphql/types/object.md#journal) is enough. For Zuzu, we'll create a single "General Ledger" journal which will record all transactions. **Request** ```graphql mutation CreateGeneralLedger { createJournal( input: { journalId: "822cb59f-ce51-4837-8391-2af3b7a5fc51" name: "General Ledger" description: "Primary journal for Zuzu." } ) { journalId name description status } } ``` **Response** ```json { "data": { "createJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "General Ledger", "description": "Primary journal for Zuzu.", "status": "ACTIVE" } } } ``` > **Note:** > > Note that Twisp ledgers come with a pre-built "Default" journal, which is ideal for single-journal ledgers. For the purposes of illustration, we'll be using a custom Journal in this tutorial. Now that we have a journal, we can start building the components needed to support the core use cases. Let's start by modeling **deposits** and **withdrawals**. --- # Step 2: Model Deposits and Withdrawals The most basic transaction types Zuzu needs to support are to allow customers to move money in and out of their checking account. In other words, we need to support deposits and withdrawals: a customer needs to be able to deposit money _into_ their checking account and withdraw some or all of their balance _out of_ their account. Our ledger will support ACH debit and credit transaction types to model "withdrawals" and "deposits". To build out this feature we'll need to do three things: - Create some customer [Accounts](https://www.twisp.com/docs/reference/graphql/types/object.md#account) to model money Zuzu holds on behalf of a customer. - Create an assets account to model Zuzu's cash assets. - Design two [TranCodes](https://www.twisp.com/docs/reference/graphql/types/object.md#tran-code) to use as a templates for ACH transactions. With double-entry accounting, every transaction needs to write _at least_ two entries to the ledger which balance out across debits and credits. *How* (with what metadata) and *where* (to which accounts) these entries are written to is determined by the type of transaction. In Twisp, transaction types are explicitly defined during the design stage by creating transaction codes, or TranCodes. > **Tip:** > > When a customer deposits money into their account, Zuzu is effectively acting as a custodian of the customer's money. This is why customer accounts are treated as a liability for the company – they represent money that Zuzu _owes_ the customer. > > The assets account represents the cash on hand that Zuzu holds at any given time. ## Create accounts First, let's create checking accounts for some sample customers. We can do this with the `createAccount` mutation. **Request** ```graphql mutation CreateCustomerAccounts { ernie_checking: createAccount( input: { accountId: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" name: "Ernie Bishop - Checking" code: "ERNIE.CHECKING" description: "Ernie's checking account" normalBalanceType: CREDIT } ) { accountId name } bert_checking: createAccount( input: { accountId: "6c6affb0-5cf5-402b-8d84-01bfc1624a2c" name: "Bert - Checking" code: "BERT.CHECKING" description: "Bert's checking account" normalBalanceType: CREDIT } ) { accountId name } } ``` **Response** ```json { "data": { "ernie_checking": { "accountId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "name": "Ernie Bishop - Checking" }, "bert_checking": { "accountId": "6c6affb0-5cf5-402b-8d84-01bfc1624a2c", "name": "Bert - Checking" } } } ``` Note that customer accounts use a credit-normal balance type because they represent liabilities. Next, let's create the assets account using a debit-normal balance type: **Request** ```graphql mutation CreateAssetsAccount { createAccount( input: { accountId: "78551b96-9c34-46f9-8d5f-c86e4459fcd7" name: "Assets" code: "ASSET" description: "Zuzu's assets (e.g. cash deposits)" normalBalanceType: DEBIT } ) { accountId name normalBalanceType } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "78551b96-9c34-46f9-8d5f-c86e4459fcd7", "name": "Assets", "normalBalanceType": "DEBIT" } } } ``` ## Check account balances Every account starts with a zero/null balance. We can check the balances of each account by querying the account id and pulling out the account balance for the primary journal we created earlier. Note that in this example, we use GraphQL variables to store the values used previously and inject them via query params. This makes it easier to re-use values across multiple requests. **Request** ```graphql query CheckAccountBalances( $ernieId: UUID! $bertId: UUID! $assetsId: UUID! $journalId: UUID! ) { ernie: account(id: $ernieId) { name balance(journalId: $journalId) { settled { normalBalance { units } } } } bert: account(id: $bertId) { name balance(journalId: $journalId) { settled { normalBalance { units } } } } assets: account(id: $assetsId) { name balance(journalId: $journalId) { settled { normalBalance { units } } } } } ``` **Response** ```json { "data": { "ernie": { "name": "Ernie Bishop - Checking", "balance": null }, "bert": { "name": "Bert - Checking", "balance": null }, "assets": { "name": "Assets", "balance": null } } } ``` **Variables** ```json { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "ernieId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "bertId": "6c6affb0-5cf5-402b-8d84-01bfc1624a2c", "assetsId": "78551b96-9c34-46f9-8d5f-c86e4459fcd7" } ``` Just as expected - balances are `null` for all accounts. Not very exciting. Let's change that. ## Design the transaction type as a TranCode The only way to write ledger entries in Twisp is by **posting a transaction**. Furthermore, every transaction is structured by the tran code used. This ensures that the ledger is consistent, predictable, and correct. To define the tran codes for ACH credits and debit transaction types, we need to first determine: - A unique identifier code for the tran code - Which accounts will be debited/credited - What entry data will be written - How we will create parameterize inputs (for values like the amount) Let's keep it simple and use the codes `ACH_CREDIT` and `ACH_DEBIT` for these tran codes. For deposits (i.e. ACH credits), we'll **credit** the customer's checking account because this account is credit-normal and represents Zuzu's obligation to the customer, and we'll **debit** the assets account because this is the debit-normal account which represents how much liquid currency Zuzu has on hand (in this case, on behalf of the customer). Withdrawals (i.e. ACH debits) are going to be basically the same, but reversed: debit the customer's checking and credit the assets account. We'll write one entry for the debit and one for the credit, using an entry type to clarify the function of the entry within the context of the transaction. Finally, we'll need to parameterize both the amount as well as the customer's checking account ID, since these are the salient pieces of information that we want to be able to provide when posting a transaction using this tran code. ### Create the TranCodes for DEPOSIT and WITHDRAW Now we can create these tran codes with GraphQL, plugging in the design decisions we just made to encode these transaction types. **Request** ```graphql mutation CreateDepositAndWithdrawalTranCodes($achCrId: UUID!, $achDrId: UUID!) { achCredit: createTranCode( input: { tranCodeId: $achCrId code: "ACH_CREDIT" description: "An ACH credit into a customer account." params: [ { name: "account", type: UUID, description: "Deposit account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "effective" type: DATE description: "Effective date for transaction." } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effective" } entries: [ { accountId: "uuid('78551b96-9c34-46f9-8d5f-c86e4459fcd7')" units: "params.amount" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } achDebit: createTranCode( input: { tranCodeId: $achDrId code: "ACH_DEBIT" description: "An ACH debit into a customer account." params: [ { name: "account", type: UUID, description: "Withdraw account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "effective" type: DATE description: "Effective date for transaction." } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effective" } entries: [ { accountId: "uuid('78551b96-9c34-46f9-8d5f-c86e4459fcd7')" units: "params.amount" currency: "'USD'" entryType: "'ACH_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "params.account" units: "params.amount" currency: "'USD'" entryType: "'ACH_DR'" direction: "DEBIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "achCredit": { "tranCodeId": "45f3f5da-034e-40c1-aaff-ab6d01bd446f" }, "achDebit": { "tranCodeId": "fab492ae-2fe4-4fcd-9bf7-cf06eb5f796b" } } } ``` **Variables** ```json { "achCrId": "45f3f5da-034e-40c1-aaff-ab6d01bd446f", "achDrId": "fab492ae-2fe4-4fcd-9bf7-cf06eb5f796b" } ``` ## Post a test transaction With these tran codes defined, we can now post transactions using them. Let's deposit $9.53 into Ernie's account: **Request** ```graphql mutation PostDeposit { postTransaction( input: { transactionId: "42847c7f-1972-4448-91b7-652c378760f4" tranCode: "ACH_CREDIT" params: { account: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" amount: "9.53" effective: "2022-09-21" } } ) { transactionId tranCodeId effective entries(first: 2) { nodes { units direction account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "42847c7f-1972-4448-91b7-652c378760f4", "tranCodeId": "45f3f5da-034e-40c1-aaff-ab6d01bd446f", "effective": "2022-09-21", "entries": { "nodes": [ { "units": "9.53", "direction": "DEBIT", "account": { "name": "Assets" } }, { "units": "9.53", "direction": "CREDIT", "account": { "name": "Ernie Bishop - Checking" } } ] } } } } ``` That all looks good. Now let's withdraw $4.28 from Ernie's account: **Request** ```graphql mutation PostACHWithdrawal { postTransaction( input: { transactionId: "39d2288d-96f9-40c7-b587-e7e75df083fa" tranCode: "ACH_DEBIT" params: { account: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" amount: "4.28" effective: "2022-09-21" } } ) { transactionId tranCodeId effective entries(first: 2) { nodes { units direction account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "39d2288d-96f9-40c7-b587-e7e75df083fa", "tranCodeId": "fab492ae-2fe4-4fcd-9bf7-cf06eb5f796b", "effective": "2022-09-21", "entries": { "nodes": [ { "units": "4.28", "direction": "CREDIT", "account": { "name": "Assets" } }, { "units": "4.28", "direction": "DEBIT", "account": { "name": "Ernie Bishop - Checking" } } ] } } } } ``` Great! We've posted our first transactions. Let's go to the next feature. --- # Step 3: Model Intra-Bank Transfers Zuzu also needs to support bank transfers between customer accounts so that customers can send money to one another. This type of transaction we'll model with a tran code called `BANK_TRANSFER`, but it's going to be a little more complex than the previous one. Zuzu isn't just letting customers transfer money for free, all day long. Instead, they'll charge a small percentage fee of 1% with a $10 maximum, paid by the sender. ## Create a revenue account The fee charged for a bank transfer will be represented as a `DEBIT` against the sender's bank account. The balancing `CREDIT` entry will be posted to Zuzu's revenue account, which doesn't exist yet. To support bank transfers, then, we'll need to first create the revenue account for Zuzu. **Request** ```graphql mutation CreateRevenueAccount($revenueId: UUID!) { createAccount( input: { accountId: $revenueId name: "Revenues" code: "REV" description: "Company revenues (e.g. fees)" normalBalanceType: CREDIT } ) { accountId name } } ``` **Response** ```json { "data": { "createAccount": { "accountId": "ece5e752-5445-4f4e-8861-d09c5c417061", "name": "Revenues" } } } ``` **Variables** ```json { "revenueId": "ece5e752-5445-4f4e-8861-d09c5c417061" } ``` Now that we have the revenue account, we can design the tran code for internal transfers. ## Define the TranCode for transfers The tran code for this transaction type needs to do a few things: - Write entries to move the defined amount from the sender's checking account to the receiver's checking account - Write an additional two entries to move the fee from the sender's checking account to the Revenue account, using a runtime expression to calculate the fee amount When posting a transaction, we want to enable the poster to provide the **sender's** account ID, the **receiver's** account ID, the **amount** to transfer, the 1% **fee**, and the **effective date** of the transfer. We'll define each of these as `params` on the TranCode. **Request** ```graphql mutation CreateBankTransferTranCode($transferId: UUID!) { createTranCode( input: { tranCodeId: $transferId code: "BANK_TRANSFER" description: "Transfer $ internally from one checking account to another. The sender is charged a 1% fee or $10, whichever is smaller." params: [ { name: "fromAccount", type: UUID, description: "Sender's account ID." } { name: "toAccount", type: UUID, description: "Receiver's account ID." } { name: "amount" type: DECIMAL description: "Amount with decimal, e.g. `1.23`." } { name: "fee" type: DECIMAL description: "Transfer fee as decimal percentage, e.g. `0.01` for 1%" } { name: "effective" type: DATE description: "Effective date for transaction." } ] transaction: { journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')" effective: "params.effective" } entries: [ { accountId: "uuid(params.fromAccount)" units: "params.amount" currency: "'USD'" entryType: "'TRANSFER_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "uuid(params.toAccount)" units: "params.amount" currency: "'USD'" entryType: "'TRANSFER_CR'" direction: "CREDIT" layer: "SETTLED" } { accountId: "uuid(params.fromAccount)" units: "decimal.Round(decimal.Mul(params.amount, params.fee), 'half_up', 2)" currency: "'USD'" entryType: "'TRANSFER_FEE_DR'" direction: "DEBIT" layer: "SETTLED" } { accountId: "uuid('ece5e752-5445-4f4e-8861-d09c5c417061')" # This is the account ID for the Revenues account units: "decimal.Round(decimal.Mul(params.amount, params.fee), 'half_up', 2)" currency: "'USD'" entryType: "'TRANSFER_FEE_CR'" direction: "CREDIT" layer: "SETTLED" } ] } ) { tranCodeId } } ``` **Response** ```json { "data": { "createTranCode": { "tranCodeId": "a0d9e35a-1df6-4f22-8e39-15c72e60b2d5" } } } ``` **Variables** ```json { "transferId": "a0d9e35a-1df6-4f22-8e39-15c72e60b2d5" } ``` > **Tip:** > > Writing clear descriptions for tran codes and their parameters is a great way to help API users can understand what the tran code is for and how to invoke it. ## Post a test transaction Let's test this transaction out by sending $2.25 from Ernie to Bert. To see the results of the transaction as encoded by the tran code, we'll return the entries posted, digging all the way down into the account for each entry. **Request** ```graphql mutation PostBankTransfer { postTransaction( input: { transactionId: "9c328550-bba3-423b-a58a-b3f9786a80ae" tranCode: "BANK_TRANSFER" params: { fromAccount: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5" toAccount: "6c6affb0-5cf5-402b-8d84-01bfc1624a2c" amount: "2.25" fee: "0.02" effective: "2022-09-10" } } ) { transactionId entries(first: 10) { nodes { units direction entryType account { name } } } } } ``` **Response** ```json { "data": { "postTransaction": { "transactionId": "9c328550-bba3-423b-a58a-b3f9786a80ae", "entries": { "nodes": [ { "units": "2.25", "direction": "DEBIT", "entryType": "TRANSFER_DR", "account": { "name": "Ernie Bishop - Checking" } }, { "units": "2.25", "direction": "CREDIT", "entryType": "TRANSFER_CR", "account": { "name": "Bert - Checking" } }, { "units": "0.05", "direction": "DEBIT", "entryType": "TRANSFER_FEE_DR", "account": { "name": "Ernie Bishop - Checking" } }, { "units": "0.05", "direction": "CREDIT", "entryType": "TRANSFER_FEE_CR", "account": { "name": "Revenues" } } ] } } } } ``` Success! From our response, we can see that each entry was posted to the correct account and for the correct amount. --- # Step 4: Organize Accounts for Balance Rollups Use account sets to group and organize accounts into a chart, taking advantage of the built-in balance aggregations. With [AccountSets](https://www.twisp.com/docs/reference/graphql/types/object.md#account-set), we can collect related accounts to provide an easy interface into summary balances and queries into the entries. We've already created checking accounts for each customer, but Zuzu also needs a way to summarize _all_ customer accounts so that we can see the total balance. To accomplish this, we'll create an account set called "Customers" and add the customer accounts to it. ## Create customers account set **Request** ```graphql mutation CreateCustomersAccountSet($customersId: UUID!, $journalId: UUID!) { createAccountSet( input: { accountSetId: $customersId journalId: $journalId name: "Customers" description: "All customer's accounts" normalBalanceType: CREDIT } ) { accountSetId journalId name description normalBalanceType } } ``` **Response** ```json { "data": { "createAccountSet": { "accountSetId": "a6ee5252-a8db-4fdc-960d-64970f3385ab", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "Customers", "description": "All customer's accounts", "normalBalanceType": "CREDIT" } } } ``` **Variables** ```json { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "customersId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } ``` Now that we have an account set, let's add the two customer accounts to it: **Request** ```graphql mutation AddCustomersToSet( $customersId: UUID! $ernieId: UUID! $bertId: UUID! ) { addErnie: addToAccountSet( id: $customersId member: { memberType: ACCOUNT, memberId: $ernieId } ) { accountSetId } addBert: addToAccountSet( id: $customersId member: { memberType: ACCOUNT, memberId: $bertId } ) { accountSetId } } ``` **Response** ```json { "data": { "addErnie": { "accountSetId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" }, "addBert": { "accountSetId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } } } ``` **Variables** ```json { "ernieId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "bertId": "6c6affb0-5cf5-402b-8d84-01bfc1624a2c", "customersId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } ``` Now that we have posted several transactions and created an account set, we can look at balances and interrogate their history to see how account balances change with each activity posted to that account. ## Review balances & interrogate history Let's start by querying for the balance of the "Customers" set, as well as the balance of each member account. **Request** ```graphql query GetCustomersBalances($customersId: UUID!, $journalId: UUID!) { accountSet(id: $customersId) { name balance { settled { normalBalance { units } } } members(first: 10) { nodes { __typename ... on Account { name balance(journalId: $journalId) { settled { normalBalance { units } } } } } } } } ``` **Response** ```json { "data": { "accountSet": { "name": "Customers", "balance": { "settled": { "normalBalance": { "units": "5.20" } } }, "members": { "nodes": [ { "__typename": "Account", "name": "Bert - Checking", "balance": { "settled": { "normalBalance": { "units": "2.25" } } } }, { "__typename": "Account", "name": "Ernie Bishop - Checking", "balance": { "settled": { "normalBalance": { "units": "2.95" } } } } ] } } } } ``` **Variables** ```json { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "customersId": "a6ee5252-a8db-4fdc-960d-64970f3385ab" } ``` As expected, the account set's balance is always equal to the sum of its member's balances. Because records in Twisp are append-only, we can review the history of any record to see how its state changed over time. Let's query the balance history of Ernie's account and compare it to the entries to see how it changed in response to transactions posted. **Request** ```graphql query GetErnieBalanceHistory($ernieId: UUID!, $journalId: UUID!) { account(id: $ernieId) { name balance(journalId: $journalId) { settled { normalBalance { units } } version history(first: 10) { nodes { version settled { normalBalance { units } } } } } entries( where: { journalId: { eq: "822cb59f-ce51-4837-8391-2af3b7a5fc51" } } first: 10 ) { nodes { entryType direction units } } } } ``` **Response** ```json { "data": { "account": { "name": "Ernie Bishop - Checking", "balance": { "history": { "nodes": [ { "settled": { "normalBalance": { "units": "2.95" } }, "version": 4 }, { "settled": { "normalBalance": { "units": "3.00" } }, "version": 3 }, { "settled": { "normalBalance": { "units": "5.25" } }, "version": 2 }, { "settled": { "normalBalance": { "units": "9.53" } }, "version": 1 } ] }, "settled": { "normalBalance": { "units": "2.95" } }, "version": 4 }, "entries": { "nodes": [ { "direction": "DEBIT", "entryType": "TRANSFER_FEE_DR", "units": "0.05" }, { "direction": "DEBIT", "entryType": "TRANSFER_DR", "units": "2.25" }, { "direction": "DEBIT", "entryType": "ACH_DR", "units": "4.28" }, { "direction": "CREDIT", "entryType": "ACH_CR", "units": "9.53" } ] } } } } ``` **Variables** ```json { "ernieId": "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5", "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## Conclusion That finalizes the tutorial! Let's recap what we did to customize and test an accounting core for the imaginary neobank Zuzu: - ✅ Modeled accounts for customers, assets, and revenue - ✅ Designed tran codes for bank transfers as well as ACH credits and debits - ✅ Organized customer accounts into a set and ran queries against it - ✅ Interrogated the balance history and entries for an account We hope that this has been useful for you to understand how Twisp works and what you can build with it. Obviously, this is an oversimplified example. Your product will certainly be more complex (and interesting)! > **Note:** > > We'd love to talk with you about your project. If you're interested, please [get in touch](https://www.twisp.com/?modal=Get+in+touch). --- # Ledger Invariants: Create and Attach Velocity Controls to Accounts In this tutorial we'll cover creating velocity controls in Twisp to ensure balances cannot exceed thresholds. ## How-to Guide: Add Velocity Controls in Twisp **Goal:** This guide shows you how to create and apply velocity controls to manage transaction limits on accounts or account sets in Twisp. **Prerequisites:** * Access to the Twisp GraphQL API. * Appropriate permissions to create/manage velocity controls and limits (`velocity.*` mutations). * UUIDs of the `Account` or `AccountSet` you wish to apply controls to. * Understanding of the specific limits you want to enforce (e.g., daily amount, transaction count). * (Optional) Familiarity with Common Expression Language (CEL) for defining complex `window`, `condition`, or `limit` expressions. ### Key Concepts * **Velocity Control:** A container that groups one or more `VelocityLimit`s. It defines an `enforcement` action (Warn, Void, Reject) and an optional `condition` (CEL expression) for when the control applies. * **Velocity Limit:** Defines a specific rule, such as a maximum amount or count within a defined `window`. It includes: * `name`/`description`: Human-readable identifiers. * `window`: CEL expressions defining the time frame or grouping criteria (e.g., daily, monthly, per merchant). Uses `PartitionKey`. * `limit`: The actual threshold (amount, layer, direction, start/end times). Uses `Limit`. * `currency`: The currency the limit applies to (or all if empty). * `condition`: Optional CEL expression for when this specific limit applies. * `params`: Optional parameters needed for dynamic limits (e.g., passing a specific merchant ID). * **Attachment:** Linking a `VelocityControl` to an `Account` or `AccountSet` makes the control active for that entity. Parameters required by the associated limits can be provided during attachment. ### Steps 1. **Define the Velocity Limit(s):** * Create each specific rule you need using the `createVelocityLimit` mutation. * Define its `name`, `description`, `window` (using `PartitionKeyInput`), `limit` (using `LimitInput`), `currency`, and optional `condition` and `params`. ```graphql # Example: Create a simple daily spending limit mutation CreateDailyLimit($limitId: UUID!) { createVelocityLimit(input: { velocityLimitId: $limitId name: "Daily Spending Limit" description: "Limit spending to $100 per day." currency: "USD" window: [{alias: "date", value: "context.vars.transaction.effective"}] # Daily window limit: { balance: [{ layer: "SETTLED" # Or PENDING, etc. amount: "decimal('100.00')" normalBalanceType: "DEBIT" # Limit debits }] } # Optional: condition: "context.vars.account.metadata.applyDailyLimit == true" # Optional: params: [{name: "maxAmount", type: DECIMAL}] # If limit amount was dynamic }) { velocityLimitId name } } ``` * *Variables:* `{ "limitId": "" }` 2. **Create the Velocity Control:** * Use the `createVelocityControl` mutation. * Define its `name`, `description`, `enforcement` action (e.g., `REJECT`), and optional overall `condition`. * Link the limits created in Step 1 using their `velocityLimitId`s in the `velocityLimitIds` array. ```graphql # Example: Create a control using the daily limit from Step 1 mutation CreateSpendingControl($controlId: UUID!, $limitId: UUID!) { createVelocityControl(input: { velocityControlId: $controlId name: "Standard Spending Control" description: "Enforces daily spending limits." enforcement: { action: REJECT } velocityLimitIds: [$limitId] # Link the limit(s) # Optional: condition: "!" + context.vars.account.metadata.exemptFromControl" }) { velocityControlId name limits { velocityLimitId } } } ``` * *Variables:* `{ "controlId": "", "limitId": "" }` 3. **Attach the Control to an Account or AccountSet:** * Use the `attachVelocityControl` mutation. * Provide the `velocityControlId` (from Step 2) and the target `accountId` (UUID of the `Account` or `AccountSet`). * If any attached limits defined `params`, provide their values in the `params` JSON object. ```graphql # Example: Attach the control to a specific account mutation AttachControlToAccount($controlId: UUID!, $accountId: UUID!) { attachVelocityControl( velocityControlId: $controlId accountId: $accountId # Optional: params: { "maxAmount": "150.00" } # If limit had params ) { velocityControlId name } } ``` * *Variables:* `{ "controlId": "", "accountId": "" }` ### Verification * Query the `VelocityControl` using its ID to confirm its limits. * Query the `Account` or `AccountSet` and check its `controls` or use the `attachedControls` query to see attached controls. * Post a transaction that should interact with the limit to ensure the expected enforcement action occurs. * Query the `velocity` field on the `Account` or `AccountSet` to check remaining limits. --- # Working with Journals In this tutorial, we will learn how to add additional journals to handle a multi-journal ledger system. Journals are a fundamental tool in accounting, used to record transactions. In this tutorial, we will cover how to create a new journal, modify an existing one, query a journal, and lock a journal to prevent posting. Every ledger in Twisp starts with a **default journal**, but you can create as many additional journals as needed to suit your particular accounting structure. > **Task:** > > - Create a new journal using the `createJournal` mutation > - Update an existing journal using the `updateJournal` mutation > - Query a journal with the `journal` query > - Delete (lock) a journal using the `deleteJournal` mutation --- ## Getting started The easiest way to interact with the Twisp GraphQL API is to login to the **Twisp Console** and use the **GraphiQL** tool. If you prefer to use your own GraphQL client, you can send authenticated requests to the Twisp API endpoint. To seed your setup with some example accounts, sets, and tran codes, you can use the [Example Setup](https://www.twisp.com/docs/tutorials/example-setup.md). ## Create a new journal You can create a new journal using the `createJournal` mutation. **Request** ```graphql mutation CreateJournal($journalGLId: UUID!) { createJournal( input: { journalId: $journalGLId name: "GL" description: "General Ledger" status: ACTIVE } ) { journalId name description status } } ``` **Response** ```json { "data": { "createJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "name": "GL", "description": "General Ledger", "status": "ACTIVE" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` This will return the ID, name, description, and status of the newly created journal. Customize the input to suit your needs. ## Modify an existing journal To modify an existing journal, you can use the `updateJournal` mutation. Let's update the description of the newly created journal: **Request** ```graphql mutation UpdateJournal($journalGLId: UUID!) { updateJournal( id: $journalGLId input: { description: "_The_ ledger. The only one." } ) { journalId description history(first: 2) { nodes { version description } } } } ``` **Response** ```json { "data": { "updateJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "description": "_The_ ledger. The only one.", "history": { "nodes": [ { "version": 2, "description": "_The_ ledger. The only one." }, { "version": 1, "description": "General Ledger" } ] } } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` The response includes the UUID and the updated description of the journal, as well as the history of the changes made to the journal. ## Query a journal To query a journal, you can use the `journal` query and provide the ID to fetch. **Request** ```graphql query GetJournal($journalGLId: UUID!) { journal(id: $journalGLId) { name description status version } } ``` **Response** ```json { "data": { "journal": { "name": "GL", "description": "General Ledger", "status": "ACTIVE", "version": 1 } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` This will return the name, description, status, and version of the journal. ## Lock a journal to prevent posting Like other resources in Twisp, "deleting" a journal does not actually remove it from the database, but instead prevents it from being used by setting its status to `LOCKED`. To lock a journal to prevent posting, you can use the `deleteJournal` mutation. Let's lock our General Ledger journal: **Request** ```graphql mutation DeleteJournal($journalGLId: UUID!) { deleteJournal(id: $journalGLId) { journalId status } } ``` **Response** ```json { "data": { "deleteJournal": { "journalId": "822cb59f-ce51-4837-8391-2af3b7a5fc51", "status": "LOCKED" } } } ``` **Variables** ```json { "journalGLId": "822cb59f-ce51-4837-8391-2af3b7a5fc51" } ``` ## Conclusion Journals are a fundamental component of accounting, used to store collections of transactions. In this tutorial, we covered how to create a new journal, modify an existing one, query a journal, and lock a journal. These basic operations can be used to manage journals effectively and efficiently in your multi-journal accounting workflow.