<img src="https://github.com/MattFaz/actualtap/raw/v1.0.33/images/logo.webp" width="200" height="200">
<i>Automatically create transactions in <a href="https://github.com/actualbudget/actual">Actual Budget</a> when you use Tap-to-Pay on a mobile device</i>
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.
Actual Tap is a Fastify API that utilizes the Actual Budget API Client to create transactions. Here's the ideal flow:
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), negative value is a deposit
"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/actualtap
services:
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:
bash
git clone https://github.com/MattFaz/actualtap.git
cd actualtap
Install dependencies:
bash
npm install
Set up your environment variables in your terminal:
bash
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:
bash
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/7d77085c7cab4278933fc6666d227fe7
The following edits must be made to the shortcut:
Note: There are comments inside the shortcut to guide you on where edits must be made
Follow the below steps to create the Automation:

Select 'New Blank Automation', then Search & add 'Dictionary'
| Item | Type | Value |
|---|---|---|
| URL | Text | https://actualtap.yourdomain.com |
| API_KEY | Text | api_key used when setting up ActualTap |
| card_name | Text | Tap 'Select Variable' then tap 'Shortcut Input'. Then Tap 'Shortcut Input' in the Value and change it to Card or Pass |
| 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'
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.
The free version of Automate allows 30 blocks to be run at once with full capability. This flo uses 11 of the 30.


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 1. Enable the sensor 2. 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_URL uses http:// or https:// 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_PASSWORD matches your Actual Budget server password
Error: Authentication failed
- Cause: Cannot authenticate with the provided credentials
- Solution:
- Verify ACTUAL_PASSWORD is correct
- Check Actual Budget server logs for authentication issues
Error: Budget '[id]' not found. Available: [list]
- Cause: The specified ACTUAL_SYNC_ID does not exist
- Solution:
- Check the list of available budget IDs in the error message
- Update ACTUAL_SYNC_ID to 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_PA
$ claude mcp add actualtap \
-- python -m otcore.mcp_server <graph>