Automatically create transactions in Actual Budget when you use Tap-to-Pay on a mobile device
Actual Tap bridges the gap between mobile payments and your Actual Budget, making expense tracking seamless and automatic. When you tap to pay with your mobile device, Actual Tap receives the transaction details and automatically creates the corresponding entry in your Actual Budget.
- 🚀 Fast and lightweight Fastify API
- 💳 Automatic transaction creation from Tap-to-Pay
- 📱 Mobile automation support (iOS Shortcuts & Android Tasker)
- 🔒 Secure API key authentication
- 🐳 Easy deployment with Docker
- 🔄 Real-time transaction syncing with Actual Budget
Actual Tap is a Fastify API that utilizes the Actual Budget API Client to create transactions. Here's the ideal flow:
- Mobile device is tapped to make a purchase
- Automation on mobile device is triggered
- POST request containing transaction information is sent to Actual Tap
- Actual Tap creates the transaction in Actual Budget
X-API-KEY: your-api-key
Content-Type: application/json
{
"account": "Checking", // Required: Name of the account in Actual Budget
"amount": 10.50, // Optional: Transaction amount (defaults to 0)
"payee": "Starbucks", // Optional: Name of the payee (defaults to "Unknown")
"type": "payment" // Optional: "payment" or "deposit" (defaults to "payment")
}# Regular transaction (expense)
curl -X POST https://actualtap.yourdomain.com/transaction \
-H "X-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"account": "Checking",
"amount": 10.50,
"payee": "Starbucks"
}'
# Deposit transaction
curl -X POST https://actualtap.yourdomain.com/transaction \
-H "X-API-KEY: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"account": "Checking",
"amount": 100.00,
"payee": "Refund",
"type": "deposit"
}'Note: ACTUAL_ENCRYPTION_PASSWORD is optional, it's only required if End-to-end encryption is Enabled on Actual Server and a password has been set.
docker run -p 3001:3001 \
-e TZ=your_timezone \
-e API_KEY=your_api_key \
-e ACTUAL_URL=your_actual_url \
-e ACTUAL_PASSWORD=your_password \
-e ACTUAL_SYNC_ID=your_budget_id \
-e ACTUAL_ENCRYPTION_PASSWORD=your_encryption_password \ # optional
mattyfaz/actualtapservices:
actualtap:
container_name: actualtap
image: mattyfaz/actualtap:latest
restart: always
ports:
- 3001:3001
environment:
- TZ=
- API_KEY=
- ACTUAL_URL=
- ACTUAL_PASSWORD=
- ACTUAL_SYNC_ID=
- ACTUAL_ENCRYPTION_PASSWORD=| Variable | Example | Description |
|---|---|---|
TZ |
Australia/Melbourne | Your timezone, ideally you should match the TZ set in Actual |
API_KEY |
527D6AAA-B22A-4D48-9DC8-C203139E5531 | Unique API key for authentication (generate with uuidgenerator.net) |
ACTUAL_URL |
https://actual.yourdomain.com | URL to Actual Budget Server |
ACTUAL_PASSWORD |
superSecretPassword | Password for your Actual Budget Server |
ACTUAL_SYNC_ID |
8B51B58D-3A0D-4B5B-A41F-DE574306A4F2 | The Unique ID of your Budget |
ACTUAL_ENCRYPTION_PASSWORD |
encryptedSecretPassword | Your Encrypted Password (optional, N/A if not using End-to-end encryption) |
-
Clone the repository:
git clone https://github.com/MattFaz/actualtap.git cd actualtap -
Install dependencies:
npm install
-
Set up your environment variables in your terminal:
export API_KEY="your-api-key" export ACTUAL_URL="your-actual-url" export ACTUAL_PASSWORD="your-password" export ACTUAL_SYNC_ID="your-budget-id" export ACTUAL_ENCRYPTION_PASSWORD="encryptedSecretPassword" # optional
-
Start the development server:
npm run dev
The app will run on port 3001 by default.
Note: Mobile setup requires ActualTap container running and publicly accessible via URL.
Setup for iOS has 2 parts, one is a Shortcut, and the second is an Automation to trigger the Shortcut upon tapping your iOS device to pay.
Click the following link to download and add the Shortcut: https://www.icloud.com/shortcuts/8565706304b645e490e56367b19a60c5
You do not need to make any edits to the Shortcut. Once added, follow the below steps to create the Automation, end result will look like the screenshot below:
-
Open Shortcuts app, select 'Automations', then '+' to create a new Automation
-
Tap 'Wallet' and Enable relevant Card, all Categories, then select 'Run Immediately'
- Do not enable 'Notify When Run'
-
Select 'New Blank Automation', then Search & add 'Dictionary'
-
Add the values below to Dictionary:
Item Type Value URL Text https://actualtap.yourdomain.com API_KEY Text api_key used when setting up ActualTap Account Text exact name of Account in Actual Budget Merchant Text Tap 'Select Variable' then tap 'Shortcut Input'. Then Tap 'Shortcut Input' in the Value and change it to Merchant Name Text Tap 'Select Variable' then tap 'Shortcut Input'. Then Tap 'Shortcut Input' in the Value and change it to Name Amount Text or Number Tap 'Select Variable' then tap 'Shortcut Input'. Then Tap 'Shortcut Input' in the Value and change it to Amount
-
-
Search & tap on 'Run Shortcut'
-
Tap 'Shortcut' and select 'ActualTap'
-
Tap the '>' to expand the action, and change 'Input' value to 'Dictionary'
Tip: Rename the card in your Google Wallet to match the account name in Actual Budget. This will allow you to use the %account variables and use multiple cards with Google Wallet and Actual Budget.
This method requires the Notification addon for Tasker.
- Add "+" a task within Tasker, and proceed to Taskernet.
- Search for "Wallet to ActualBudget" and import the task.
- Import by long pressing on "PROFILES"
- Search for "Wallet to ActualBudget" and import the task.
- Navigate to the "TASKS" tab and edit "Send To ActualTap"
- Edit the HTTP Request step
- Replace URL with http://{your-actual-tap-address.com}/transaction
- Add your API key to HEADERS
- Body:
- Remove the [ ] brackets.
The free version of Automate allows 30 blocks to be run at once with full capability. This flo uses 11 of the 30.
- Download the flo for Automate. https://llamalab.com/automate/community/flows/50847
- This can be searched for within the Automate app on your mobile device.
- Edit the "HTTP request" block
- Update the Request URL to your actualtap address
- Update your API key for your actualtap deployment
- Save your changes and start flo.
Summary of flo
- The flo will pause until a new notification appears.
- If the notification is Google Wallet, proceed.
- Set two variables. One for payee and one that contains account and amount information.
- Get current date
- Use a REGEX pattern to extract the amount information.
- Pass the amount, payee, and date information to actualtap using the HTTP request block.
- If the httprequest was successful, returns 200, remove the notification.
- If the httprequest failed, leave the notification and return to wait for a new notification.
If a request failed, you can change the Notification block to activate "Immediately" to process it. Then change it back to "When transition"
Navigate to Settings -> Companion App -> Manage Sensors Select Last Notification
- Enable the sensor
- Select Allow list. Select any apps that will be used for posting transactions. Google Wallet is typically selected.
Add a section in configuration.yaml and update your actualtap url.
rest_command:
actualbudget:
url: "https://actualtap.example.com/transaction"
method: post
content_type: 'application/json'
headers:
X-API-KEY: !secret actualtap_api
payload: '{"account": "{{accountVar}}", "amount": "{{amountVar}}", "date": "{{dateVar}}", "payee": "{{payeeVar}}", "notes": "{{notesVar}}"}'
Add your api key.
actualtap_api: "YOUR API KEY"
Navigate to Settings -> Automations and Scenes within Home Assistant.
Create a new Automation
Use the three dot menu at the top to "Edit in YAML".
Paste the following code. Replace "your_device" with your devices name.
alias: Google Wallet Transaction Automation
description: ""
triggers:
- entity_id: sensor.your_device_last_notification
trigger: state
actions:
- data:
accountVar: >
{% set text = state_attr('sensor.your_device_last_notification',
'android.text') %} {% if text %}
{{ text.split(' with ')[1] if ' with ' in text else 'Unknown Account' }}
{% else %}
'Unknown Account'
{% endif %}
amountVar: >
{% set text = state_attr('sensor.your_device_last_notification',
'android.text') %} {% if text %}
{% set match = text | regex_findall('\$([0-9]+\.[0-9]{2})') %}
{{ match[0] if match else '0.00' }}
{% else %}
'0.00'
{% endif %}
dateVar: "{{ now().date() }}"
payeeVar: >-
{{ state_attr('sensor.your_device_last_notification', 'android.title')
}}
notesVar: Added with Home Assistant
response_variable: httpresponse
action: rest_command.actualbudget
- data:
level: info
message: "REST Response: {{ httpresponse }}"
action: system_log.write
Actual Tap was developed with Mobile Tap-to-Pay as the main use case. In order for that to function Actual Tap needs to be exposed to the internet. Below is a standard Caddyfile configuration:
actualtap.yourdomain.com {
@auth header X-API-KEY your-api-key
handle @auth {
reverse_proxy 0.0.0.0:3001
}
respond 401
}
Error: Invalid ACTUAL_URL format
- Cause: The URL is malformed or uses an invalid protocol
- Solution: Ensure
ACTUAL_URLuseshttp://orhttps://protocol (e.g.,https://actual.yourdomain.com)
Error: Connection timed out - check if server is accessible
- Cause: Cannot reach the Actual Budget server within 5 seconds
- Solution:
- Verify the server is running and accessible
- Check network connectivity
- Verify firewall rules allow the connection
Error: Cannot resolve hostname - check if ACTUAL_URL is correct
- Cause: DNS cannot resolve the hostname
- Solution: Verify the URL is correct and the domain exists
Error: Connection refused - check if server is running
- Cause: Server is not accepting connections on the specified port
- Solution: Verify Actual Budget server is running and listening on the correct port
Error: Server returned HTTP [status code]
- Cause: Server responded but with an error status
- Solution: Check Actual Budget server logs for issues
Error: ACTUAL_PASSWORD is incorrect (no budgets found)
- Cause: The password provided is invalid
- Solution: Verify
ACTUAL_PASSWORDmatches your Actual Budget server password
Error: Authentication failed
- Cause: Cannot authenticate with the provided credentials
- Solution:
- Verify
ACTUAL_PASSWORDis correct - Check Actual Budget server logs for authentication issues
- Verify
Error: Budget '[id]' not found. Available: [list]
- Cause: The specified
ACTUAL_SYNC_IDdoes not exist - Solution:
- Check the list of available budget IDs in the error message
- Update
ACTUAL_SYNC_IDto use one of the available IDs - You can find your budget's Sync ID in Actual Budget under Settings → Show advanced settings → Sync ID
Error: ACTUAL_ENCRYPTION_PASSWORD is incorrect
- Cause: The encryption password is wrong or the budget is not encrypted
- Solution:
- If your budget uses end-to-end encryption, verify
ACTUAL_ENCRYPTION_PASSWORDis correct - If your budget is not encrypted, remove the
ACTUAL_ENCRYPTION_PASSWORDenvironment variable
- If your budget uses end-to-end encryption, verify
Error: Failed to download budget
- Cause: General error downloading the budget
- Solution:
- Check Actual Budget server logs
- Verify network connectivity
- Try restarting ActualTap
Error: Budget failed to open. This is likely due to a version mismatch...
- Cause: The Actual Budget server version is newer than the @actual-app/api version bundled with ActualTap
- Solution:
- Update ActualTap to the latest version that matches your Actual Budget server version
- Check the releases page for version compatibility
Error: 401 Unauthorized
- Cause: Missing or invalid API key
- Solution: Ensure the
X-API-KEYheader matches theAPI_KEYenvironment variable
Error: Account '[name]' not found
- Cause: The specified account does not exist in your budget
- Solution:
- Verify the account name in your request exactly matches an account in Actual Budget (case-sensitive)
- Check for extra spaces or typos in the account name
Error: Initialization timed out after 30 seconds
- Cause: Actual API took too long to initialize
- Solution:
- Check network connectivity to Actual Budget server
- Verify server is responsive
- Try restarting ActualTap
ActualTap uses pretty-printed logs for easier troubleshooting. To view logs:
Docker:
docker logs actualtapDocker Compose:
docker compose logs actualtapLogs will show detailed information about the initialization process, including which step failed and why.
Note: This project is in active development. Issues, pull requests, and feature requests are welcome.



