Whisper Money

Search the documentation

Search pages and sections.

Automation rules

An automation rule gives a category, labels and a note to the transactions that meet its conditions. Rules are evaluated in priority order, lowest first, and a new transaction gets the actions of the first rule it matches and no other. A rule only runs on transactions created after it: run it over the ones already there with the apply endpoint.

The example requests read your personal token from $WHISPER_MONEY_TOKEN: run export WHISPER_MONEY_TOKEN=wm_api_your_token_here before pasting them.

List automation rules

GET /api/v1/automation-rules

Your rules in a space in the order they are evaluated: priority ascending, then title. As in the app, another member's rules are not listed.

Scope: read. A personal token needs api:read; an OAuth token needs api:use.

Parameters

Name In Type Required Description
limit query integer No How many objects to return, between 1 and 100. Defaults to 25.
starting_after query string (uuid) No An object id: return the page that follows it. Pass the last id of the page you have.
ending_before query string (uuid) No An object id: return the page that precedes it. Pass the first id of the page you have. Cannot be combined with starting_after.
space query string (uuid) No The id of the space to work in (see GET /spaces). Defaults to your personal space. On writes it may also be sent in the JSON body.

Response

200

Field Type Description
data array of AutomationRule
data[].id string (uuid) The rule's id.
data[].space_id string (uuid) or null The id of the space the rule is in.
data[].title string The rule's name.
data[].priority integer The order the rule is evaluated in, lowest first. A new transaction gets the actions of the first rule it matches, and no other.
data[].conditions object The conditions a transaction must meet, as one JsonLogic object. Amounts in here are in MAJOR units, unlike every other amount in the API. A rule the AI categorization learned can still have none: {}.
data[].action_category_id string (uuid) or null The id of the category the rule gives the matching transactions, or null for none.
data[].action_label_ids array of string The ids of the labels the rule adds to the matching transactions.
data[].action_note string or null The note the rule appends to the matching transactions' notes, or null for none.
data[].origin "user" | "ai" | "correction" Who made the rule: user for one made by hand (in the app, the AI connector or this API), ai for one Whisper Money's AI categorization learned, and correction for one it learned from your own recategorizations.
data[].created_at string (date-time) or null When the rule was created.
data[].updated_at string (date-time) or null When the rule was last changed.
has_more boolean Whether more objects exist past this page, in the direction being walked.

Example request

curl https://whisper.money/api/v1/automation-rules \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN"

Example response

{
    "data": [
        {
            "id": "0199a3f9-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
            "space_id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
            "title": "Groceries",
            "priority": 10,
            "conditions": {
                "and": [
                    {
                        "in": [
                            "mercadona",
                            {
                                "var": "description"
                            }
                        ]
                    },
                    {
                        "<": [
                            {
                                "var": "amount"
                            },
                            0
                        ]
                    }
                ]
            },
            "action_category_id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
            "action_label_ids": [
                "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
            ],
            "action_note": "Weekly shop",
            "origin": "user",
            "created_at": "2026-01-15T09:30:00+00:00",
            "updated_at": "2026-01-20T18:05:00+00:00"
        }
    ],
    "has_more": true
}

Errors

Status When
400 A malformed request: an unknown pagination cursor (invalid_cursor) or an invalid Idempotency-Key (invalid_idempotency_key).
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Create an automation rule

POST /api/v1/automation-rules

The rule needs at least one action: action_category_id, action_label_ids or both. It only runs on the transactions created from now on. Conditions that say something other than what you meant still save and then quietly match nothing, so preview a new rule with the apply endpoint before trusting it.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
space query string (uuid) No The id of the space to work in (see GET /spaces). Defaults to your personal space. On writes it may also be sent in the JSON body.
Idempotency-Key header string No Any unique string up to 255 characters. Retrying with the same key within 24 hours returns the first response instead of repeating the request.

Request body

Field Type Required Description
title string Yes The rule's name.
priority integer Yes The order the rule is evaluated in, lowest first. A new transaction gets the actions of the first rule it matches, and no other.
conditions object Yes The conditions a transaction must meet, as one JsonLogic object. Amounts in here are in MAJOR units (12.50), unlike every other amount in the API, and signed: an expense is negative. Text is compared lowercased, so write values in lowercase. Only these operators are accepted: and, or, !, ==, !=, >, >=, <, <=, in and var; and only these variables: description, notes, creditor_name, debtor_name, account_name, bank_name, category, transaction_date (YYYY-MM-DD) and amount. "Contains" is {"in": ["netflix", {"var": "description"}]}, a negation wraps a condition in !, and and / or take a list and nest freely. "Is not" and "does not contain" also match a transaction whose field is empty.
action_category_id string (uuid) or null No The id of the category to give the matching transactions, one of your categories in the space, or null for none. A rule needs a category, labels or both.
action_label_ids array of string (uuid) No The ids of the labels to add to the matching transactions, each one of your labels in the space, 50 at most. On update the list replaces the rule's labels; an empty one removes them all.
action_note string or null No A note to append to the matching transactions' notes, or null for none.

Response

201

Field Type Description
data AutomationRule
data.id string (uuid) The rule's id.
data.space_id string (uuid) or null The id of the space the rule is in.
data.title string The rule's name.
data.priority integer The order the rule is evaluated in, lowest first. A new transaction gets the actions of the first rule it matches, and no other.
data.conditions object The conditions a transaction must meet, as one JsonLogic object. Amounts in here are in MAJOR units, unlike every other amount in the API. A rule the AI categorization learned can still have none: {}.
data.action_category_id string (uuid) or null The id of the category the rule gives the matching transactions, or null for none.
data.action_label_ids array of string The ids of the labels the rule adds to the matching transactions.
data.action_note string or null The note the rule appends to the matching transactions' notes, or null for none.
data.origin "user" | "ai" | "correction" Who made the rule: user for one made by hand (in the app, the AI connector or this API), ai for one Whisper Money's AI categorization learned, and correction for one it learned from your own recategorizations.
data.created_at string (date-time) or null When the rule was created.
data.updated_at string (date-time) or null When the rule was last changed.

Example request

curl -X POST https://whisper.money/api/v1/automation-rules \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Groceries",
    "priority": 10,
    "conditions": {
        "and": [
            {
                "in": [
                    "mercadona",
                    {
                        "var": "description"
                    }
                ]
            },
            {
                "<": [
                    {
                        "var": "amount"
                    },
                    0
                ]
            }
        ]
    },
    "action_label_ids": [
        "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
    ],
    "action_note": "Weekly shop"
}'

Example response

{
    "data": {
        "id": "0199a3f9-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
        "space_id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
        "title": "Groceries",
        "priority": 10,
        "conditions": {
            "and": [
                {
                    "in": [
                        "mercadona",
                        {
                            "var": "description"
                        }
                    ]
                },
                {
                    "<": [
                        {
                            "var": "amount"
                        },
                        0
                    ]
                }
            ]
        },
        "action_category_id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
        "action_label_ids": [
            "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
        ],
        "action_note": "Weekly shop",
        "origin": "user",
        "created_at": "2026-01-15T09:30:00+00:00",
        "updated_at": "2026-01-20T18:05:00+00:00"
    }
}

Errors

Status When
400 A malformed request: an unknown pagination cursor (invalid_cursor) or an invalid Idempotency-Key (invalid_idempotency_key).
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
409 A request with the same Idempotency-Key is still being processed.
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Retrieve an automation rule

GET /api/v1/automation-rules/{automation_rule}

Scope: read. A personal token needs api:read; an OAuth token needs api:use.

Parameters

Name In Type Required Description
automation_rule path string (uuid) Yes The rule's id.
space query string (uuid) No The id of the space to work in (see GET /spaces). Defaults to your personal space. On writes it may also be sent in the JSON body.

Response

200

Field Type Description
data AutomationRule
data.id string (uuid) The rule's id.
data.space_id string (uuid) or null The id of the space the rule is in.
data.title string The rule's name.
data.priority integer The order the rule is evaluated in, lowest first. A new transaction gets the actions of the first rule it matches, and no other.
data.conditions object The conditions a transaction must meet, as one JsonLogic object. Amounts in here are in MAJOR units, unlike every other amount in the API. A rule the AI categorization learned can still have none: {}.
data.action_category_id string (uuid) or null The id of the category the rule gives the matching transactions, or null for none.
data.action_label_ids array of string The ids of the labels the rule adds to the matching transactions.
data.action_note string or null The note the rule appends to the matching transactions' notes, or null for none.
data.origin "user" | "ai" | "correction" Who made the rule: user for one made by hand (in the app, the AI connector or this API), ai for one Whisper Money's AI categorization learned, and correction for one it learned from your own recategorizations.
data.created_at string (date-time) or null When the rule was created.
data.updated_at string (date-time) or null When the rule was last changed.

Example request

curl https://whisper.money/api/v1/automation-rules/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60 \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN"

Example response

{
    "data": {
        "id": "0199a3f9-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
        "space_id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
        "title": "Groceries",
        "priority": 10,
        "conditions": {
            "and": [
                {
                    "in": [
                        "mercadona",
                        {
                            "var": "description"
                        }
                    ]
                },
                {
                    "<": [
                        {
                            "var": "amount"
                        },
                        0
                    ]
                }
            ]
        },
        "action_category_id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
        "action_label_ids": [
            "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
        ],
        "action_note": "Weekly shop",
        "origin": "user",
        "created_at": "2026-01-15T09:30:00+00:00",
        "updated_at": "2026-01-20T18:05:00+00:00"
    }
}

Errors

Status When
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Update an automation rule

PATCH /api/v1/automation-rules/{automation_rule}

Changes only the fields you send. conditions and action_label_ids replace the rule's whole set. The rule must keep at least one action, a category or labels: leaving it without one is a 422 under action_category_id. The transactions it already touched keep what it gave them.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
automation_rule path string (uuid) Yes The rule's id.
space query string (uuid) No The id of the space to work in (see GET /spaces). Defaults to your personal space. On writes it may also be sent in the JSON body.

Request body

Field Type Required Description
title string No The rule's name.
priority integer No The order the rule is evaluated in, lowest first. A new transaction gets the actions of the first rule it matches, and no other.
conditions object No The conditions a transaction must meet, as one JsonLogic object. Amounts in here are in MAJOR units (12.50), unlike every other amount in the API, and signed: an expense is negative. Text is compared lowercased, so write values in lowercase. Only these operators are accepted: and, or, !, ==, !=, >, >=, <, <=, in and var; and only these variables: description, notes, creditor_name, debtor_name, account_name, bank_name, category, transaction_date (YYYY-MM-DD) and amount. "Contains" is {"in": ["netflix", {"var": "description"}]}, a negation wraps a condition in !, and and / or take a list and nest freely. "Is not" and "does not contain" also match a transaction whose field is empty.
action_category_id string (uuid) or null No The id of the category to give the matching transactions, one of your categories in the space, or null for none. A rule needs a category, labels or both.
action_label_ids array of string (uuid) No The ids of the labels to add to the matching transactions, each one of your labels in the space, 50 at most. On update the list replaces the rule's labels; an empty one removes them all.
action_note string or null No A note to append to the matching transactions' notes, or null for none.

Response

200

Field Type Description
data AutomationRule
data.id string (uuid) The rule's id.
data.space_id string (uuid) or null The id of the space the rule is in.
data.title string The rule's name.
data.priority integer The order the rule is evaluated in, lowest first. A new transaction gets the actions of the first rule it matches, and no other.
data.conditions object The conditions a transaction must meet, as one JsonLogic object. Amounts in here are in MAJOR units, unlike every other amount in the API. A rule the AI categorization learned can still have none: {}.
data.action_category_id string (uuid) or null The id of the category the rule gives the matching transactions, or null for none.
data.action_label_ids array of string The ids of the labels the rule adds to the matching transactions.
data.action_note string or null The note the rule appends to the matching transactions' notes, or null for none.
data.origin "user" | "ai" | "correction" Who made the rule: user for one made by hand (in the app, the AI connector or this API), ai for one Whisper Money's AI categorization learned, and correction for one it learned from your own recategorizations.
data.created_at string (date-time) or null When the rule was created.
data.updated_at string (date-time) or null When the rule was last changed.

Example request

curl -X PATCH https://whisper.money/api/v1/automation-rules/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60 \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Groceries",
    "priority": 10,
    "conditions": {
        "and": [
            {
                "in": [
                    "mercadona",
                    {
                        "var": "description"
                    }
                ]
            },
            {
                "<": [
                    {
                        "var": "amount"
                    },
                    0
                ]
            }
        ]
    },
    "action_label_ids": [
        "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
    ],
    "action_note": "Weekly shop"
}'

Example response

{
    "data": {
        "id": "0199a3f9-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
        "space_id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
        "title": "Groceries",
        "priority": 10,
        "conditions": {
            "and": [
                {
                    "in": [
                        "mercadona",
                        {
                            "var": "description"
                        }
                    ]
                },
                {
                    "<": [
                        {
                            "var": "amount"
                        },
                        0
                    ]
                }
            ]
        },
        "action_category_id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
        "action_label_ids": [
            "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
        ],
        "action_note": "Weekly shop",
        "origin": "user",
        "created_at": "2026-01-15T09:30:00+00:00",
        "updated_at": "2026-01-20T18:05:00+00:00"
    }
}

Errors

Status When
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Delete an automation rule

DELETE /api/v1/automation-rules/{automation_rule}

The rule stops running on new transactions. The transactions it already categorized or labelled keep their category and labels.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
automation_rule path string (uuid) Yes The rule's id.
space query string (uuid) No The id of the space to work in (see GET /spaces). Defaults to your personal space. On writes it may also be sent in the JSON body.

Response

200

Field Type Description
id string (uuid) The deleted rule's id.
deleted boolean Always true.

Example request

curl -X DELETE https://whisper.money/api/v1/automation-rules/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60 \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN"

Example response

{
    "id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
    "deleted": true
}

Errors

Status When
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Apply an automation rule to existing transactions

POST /api/v1/automation-rules/{automation_rule}/apply

Runs the rule over the transactions already in the space, which it otherwise never touches. By default this is a preview (dry_run: true): it answers status: preview with how many transactions match and the first 20 of them, newest first, and changes nothing. With dry_run: false it overwrites your data: every match gets the rule's category (replacing the one it had when only_uncategorized is false), its labels added and its note appended. It cannot be undone. Up to 100 matches are applied right away and answer 200 with status: done; more are applied in the background over the next few minutes and answer 202 with status: queued. There is no progress endpoint: preview again to see what the rule still matches. Previewing and applying both need a write token, and both only reach your own transactions. The matches are worked out once and reused for up to 15 minutes, so the apply that follows a preview goes over the transactions that preview reported; editing the rule starts over.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
automation_rule path string (uuid) Yes The rule's id.
space query string (uuid) No The id of the space to work in (see GET /spaces). Defaults to your personal space. On writes it may also be sent in the JSON body.
Idempotency-Key header string No Any unique string up to 255 characters. Retrying with the same key within 24 hours returns the first response instead of repeating the request.

Request body

Field Type Required Description
dry_run boolean No true (the default) only previews: it reports how many transactions the rule matches, with a sample of them, and changes nothing. Send false to apply the rule's actions to every match.
only_uncategorized boolean No true (the default) skips the transactions that already have a category or, for a rule with labels and no category, that already carry all its labels. Send false to also take the categorized ones: the rule then overwrites the category the user or another rule gave them. Preview that combination before applying it.

Response

200

Field Type Description
data RuleApplication
data.status "preview" | "done" | "queued" preview when nothing was changed, done when the rule was applied to every match, queued when it is being applied in the background.
data.automation_rule_id string (uuid) The rule's id.
data.total integer How many transactions the rule matches.
data.sample array of Transaction On a preview, the first 20 matches, newest first; empty otherwise.
data.sample[].id string (uuid) The transaction's id.
data.sample[].space_id string (uuid) or null The id of the space the transaction is in, its account's.
data.sample[].account object The account the transaction is on: its id and name.
data.sample[].account.id string
data.sample[].account.name string
data.sample[].transaction_date string (date) The day the transaction happened.
data.sample[].description string What the transaction was for.
data.sample[].amount integer The signed amount, in the minor units of currency: negative for an expense, positive for income.
data.sample[].currency string The ISO 4217 currency the amount is in.
data.sample[].category object or null The category, if one is set: its id and name.
data.sample[].category.id string
data.sample[].category.name string
data.sample[].category_source "manual" | "rule" | "ai" | "bank" | null Who set the category: manual (you), rule (an automation rule), ai (the automatic categorization) or bank. null while there is none.
data.sample[].labels array of object The labels the transaction is tagged with: their ids and names.
data.sample[].labels[].id string
data.sample[].labels[].name string
data.sample[].source "manually_created" | "imported" | "enablebanking" | "wise" Where the transaction came from: manually_created (recorded by hand), imported (from a file), enablebanking or wise (a bank sync). Only a manually-created one can change its description, amount, currency, account or counterparty names; notes and the category can be changed on any, the date on any but one part of a split, and any but one part of a split can be deleted.
data.sample[].creditor_name string or null Who was paid, or null.
data.sample[].debtor_name string or null Who paid, or null.
data.sample[].notes string or null Free-text notes, or null.
data.sample[].split_parent_id string (uuid) or null Set when this transaction is one part of a split, otherwise null. Every part of the same split carries the same value. The split original is out of every list, and a part cannot be deleted or have its core fields changed on its own.
data.sample[].created_at string (date-time) or null When the transaction was recorded in Whisper Money.
data.applied integer or null When done, how many matches the rule went over; null otherwise.
data.updated integer or null When done, how many of them it changed: one that already had the rule's category, labels and note is applied but not updated. null otherwise.

Example request

curl -X POST https://whisper.money/api/v1/automation-rules/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60/apply \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dry_run": true
}'

Example response

{
    "data": {
        "status": "preview",
        "automation_rule_id": "0199a3f9-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
        "total": 42,
        "sample": [
            {
                "id": "0199a3f8-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
                "space_id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
                "account": {
                    "id": "0199a3f4-1d2e-7c3b-8a4f-5e6d7c8b9a01",
                    "name": "Main checking"
                },
                "transaction_date": "2026-01-31",
                "description": "Groceries at the market",
                "amount": -4250,
                "currency": "EUR",
                "category": {
                    "id": "0199a3f5-7a8b-7c9d-8e0f-1a2b3c4d5e6f",
                    "name": "Groceries"
                },
                "category_source": "manual",
                "labels": [
                    {
                        "id": "0199a3f7-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
                        "name": "Holidays"
                    }
                ],
                "source": "manually_created",
                "creditor_name": "Fresh Market",
                "debtor_name": null,
                "notes": null,
                "split_parent_id": null,
                "created_at": "2026-01-31T18:12:00+00:00"
            }
        ],
        "applied": null,
        "updated": null
    }
}

Errors

Status When
400 A malformed request: an unknown pagination cursor (invalid_cursor) or an invalid Idempotency-Key (invalid_idempotency_key).
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
409 A request with the same Idempotency-Key is still being processed.
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.