api REST API

EzFin API Documentation

Complete reference for integrating mobile apps and services with EzFin.

download Download .md https://ezfin.app/api

Last updated 2026-10-03

EzFin REST API

Version: 1.1 · Last verified: 2026-10-03 Base URL: https://ezfin.app/api Authentication: Laravel Sanctum bearer tokens Format: JSON (Accept: application/json)

Every endpoint in this document is covered by the automated suite in tests/Feature/Api (50 tests, 669 assertions), which exercises the request and response shapes described here.


Table of Contents

  1. Conventions
  2. Authentication
  3. User Profile
  4. Accounts
  5. Categories
  6. Transactions
  7. Recurring Transactions
  8. Budgets
  9. Financial Goals
  10. Customers & Suppliers
  11. Accounts Receivable & Payable
  12. Payments
  13. Reports
  14. Settings
  15. Dashboard
  16. Notifications
  17. Mobile
  18. Admin API
  19. Endpoint Index
  20. Quick Start

Conventions

Headers

Header Value When
Accept application/json Always. Without it, errors may come back as HTML redirects instead of JSON.
Content-Type application/json Requests with a JSON body
Content-Type multipart/form-data File uploads (transaction attachment, avatar)
Authorization Bearer {token} Every endpoint except /test, /register and /login

Identifiers

All IDs are UUID strings (for example "01a1029e-d31c-700e-bab1-474f5458ea23"), never integers.

Data types

  • Money. Amounts stored in the database (amount, initial_balance, target_amount, paid_amount, …) are returned as decimal strings such as "1200.00". Values computed by the API (current_balance, spent, progress, report totals, …) are JSON numbers. Parse both as decimals.
  • Dates. Send dates as YYYY-MM-DD and times as HH:mm. Date fields come back as ISO 8601 timestamps ("2026-03-06T00:00:00.000000Z"). Report labels and upcoming dates are plain YYYY-MM-DD strings.
  • Booleans. Query-string flags accept 1/0 and true/false.

Success responses

{
  "success": true,
  "data": { },
  "message": "Optional human-readable message"
}

201 Created is returned when a resource is created; everything else that succeeds returns 200 OK.

Pagination

There are two pagination shapes.

Laravel paginator. Used by /transactions, /accounts-receivable, /accounts-payable, /payments and /reports/saved:

{
  "success": true,
  "data": {
    "current_page": 1,
    "data": [ ],
    "first_page_url": "https://ezfin.app/api/transactions?page=1",
    "from": 1,
    "last_page": 4,
    "last_page_url": "https://ezfin.app/api/transactions?page=4",
    "links": [ ],
    "next_page_url": "https://ezfin.app/api/transactions?page=2",
    "path": "https://ezfin.app/api/transactions",
    "per_page": 25,
    "prev_page_url": null,
    "to": 25,
    "total": 92
  }
}

Compact. Used by /notifications and the admin lists:

{
  "success": true,
  "data": {
    "items_key": [ ],
    "pagination": { "current_page": 1, "per_page": 15, "total": 25, "last_page": 2 }
  }
}

Use ?page=N to move between pages. per_page is clamped to the range 1–100.

Errors

Status Meaning Body
401 Missing, invalid or revoked token {"message": "Unauthenticated."}
403 Resource belongs to someone else {"success": false, "message": "Unauthorized"}
403 Not an admin (on /admin/*) {"message": "Unauthorized action."}
403 Account suspended {"success": false, "message": "Your account has been suspended."}
404 Unknown ID or route {"message": "No query results for model [App\\Models\\Account] {id}"}
400 Request understood but not allowed (business rule) {"success": false, "message": "..."}
422 Validation failed see below
500 Unexpected server error {"message": "Server Error"}

Validation errors come in one of two shapes, depending on the endpoint:

{ "success": false, "errors": { "email": ["The email field must be a valid email address."] } }
{ "message": "The provided credentials are incorrect.", "errors": { "email": ["The provided credentials are incorrect."] } }

The second shape is used by /login, /mobile/sync and every /admin/* endpoint. Both shapes always include an errors object keyed by field name.

Supported values

  • Languages: en, pt-br, es
  • Currencies: USD, EUR, BRL, GBP, CAD, AUD, JPY, CHF, CNY, MXN
  • Themes: light, dark, system

Rate limiting

No request rate limit is currently enforced. If you receive 429 Too Many Requests, back off and retry.

Health check

GET /api/test

No authentication. Returns {"message": "API is working", "timestamp": "..."}.


Authentication

Register

POST /api/register
Field Type Required Rules
name string yes max 255
email string yes valid e-mail, unique
password string yes min 8
password_confirmation string yes must match password
language string no en (default), pt-br, es
currency string no a supported currency, default USD (stored as preferred_currency)
theme string no light (default), dark, system

Registering through the API behaves like signing up on the website: the account gets the default categories and a welcome e-mail.

201

{
  "success": true,
  "data": {
    "user": {
      "id": "01a1029e-d2c7-711e-8442-841f018a1a39",
      "name": "John Doe",
      "email": "john@example.com",
      "language": "en",
      "preferred_currency": "USD",
      "created_at": "2026-10-03T16:35:25.000000Z",
      "updated_at": "2026-10-03T16:35:25.000000Z"
    },
    "access_token": "01a1029e-d2df-710e-90cd-d0d9e261c9a9|t1Gnr0LjNcUfLnA9AfOfxJWmX2L0NFGxph09YHRa72d80122",
    "token_type": "Bearer"
  },
  "message": "User registered successfully"
}

Login

POST /api/login
Field Type Required Notes
email string yes
password string yes
device_name string no Stored as the token name (defaults to the User-Agent)
revoke_existing boolean no Revoke every other token of this user first

200. Same data shape as Register (user, access_token, token_type), with the message "Logged in successfully".

422. Wrong e-mail or password:

{ "message": "The provided credentials are incorrect.", "errors": { "email": ["The provided credentials are incorrect."] } }

403. The account was suspended by an administrator.

Current user

GET /api/user
GET /api/profile        (alias)

Returns the user together with their setting record.

{
  "success": true,
  "data": {
    "id": "01a1029e-d2a7-72e2-8251-84cfc64c6df1",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": null,
    "language": "en",
    "preferred_currency": "USD",
    "is_admin": false,
    "avatar": null,
    "suspended_at": null,
    "setting": {
      "language": "en",
      "currency": "USD",
      "date_format": "Y-m-d",
      "time_format": "H:i",
      "theme": "light",
      "notifications_enabled": true,
      "notification_preferences": null,
      "device_token": null,
      "device_type": null
    }
  }
}

Logout

POST /api/logout

Revokes the token used for the request. 200 {"success": true, "message": "Logged out successfully"}

Refresh token

POST /api/refresh

Revokes the current token and issues a new one with the same name.

{ "success": true, "data": { "access_token": "…|…", "token_type": "Bearer" }, "message": "Token refreshed successfully" }

List tokens

GET /api/tokens
{
  "success": true,
  "data": [
    {
      "id": "01a1029e-d30f-7259-999f-36fc83cce91b",
      "name": "iPhone 15",
      "abilities": ["*"],
      "last_used_at": null,
      "expires_at": null,
      "created_at": "2026-10-03T16:35:25.000000Z"
    }
  ]
}

Create token

POST /api/tokens
Field Type Required
name string yes, max 255

201

{ "success": true, "data": { "token": "01a1029e-…|SbWn0qeh…", "token_id": "01a1029e-d30f-7259-999f-36fc83cce91b" }, "message": "Token created successfully" }

The plain-text token is shown only once.

Delete token

DELETE /api/tokens/{token_id}

200 {"success": true, "message": "Token deleted successfully"}. Returns 404 if the token does not exist or belongs to another user.


User Profile

Update profile

PUT /api/profile

Send only the fields you want to change.

Field Type Rules
name string max 255
email string valid e-mail, unique
language string en, pt-br, es
preferred_currency string supported currency
phone string|null max 20
settings object optional, see below
settings.theme string light, dark, system
settings.date_format string Y-m-d, d/m/Y, m/d/Y, d-m-Y, m-d-Y
settings.time_format string H:i, h:i A
settings.notifications_enabled boolean
settings.notification_preferences object|null

Unknown keys inside settings are ignored. 200 returns the user with setting, and the message "Profile updated successfully".

Change password

POST /api/profile/password
Field Type Required Notes
current_password string yes
password string yes min 8, must be confirmed
password_confirmation string yes
revoke_other_sessions boolean no Revoke every token except the current one

200 {"success": true, "message": "Password updated successfully"}. A wrong current_password returns 422 with errors.current_password.

Upload avatar

POST /api/profile/avatar
Content-Type: multipart/form-data
Field Type Rules
avatar file jpeg, png, jpg or gif, max 2 MB
{ "success": true, "data": { "avatar_url": "https://ezfin.app/storage/avatars/abc123.jpg" }, "message": "Avatar uploaded successfully" }

Uploading a new avatar replaces and deletes the previous file.

Delete account

POST /api/profile/delete-account
Field Type Rules
password string the current password
confirmation string must be exactly DELETE

200 {"success": true, "message": "Account deleted successfully"}. This permanently removes the user, every record they own, and all of their tokens. A wrong password returns 422.


Accounts

Accounts can be owned by the user or shared with them by another user. A shared account carries a role: viewer (read-only) or editor (can edit the account and add or edit its transactions). Only the owner can delete an account.

Account object

Field Type Notes
id uuid
name string
type string checking, savings, cash, credit_card, investment, other
initial_balance decimal
currency string 3 letters
icon / color string|null Material icon name / hex colour
is_active boolean
include_in_totals boolean Counted in /accounts/balances and the dashboard total
closing_day / due_day int|null Credit cards only (1–31)
current_balance number initial_balance + income − expense. Transfers are not counted.
user_role string owner, or the role on a shared account
is_owner / is_shared boolean
owner_name / owner_email string Shared accounts only

List accounts

GET /api/accounts
Query Type Description
active true Only active accounts
include_balance boolean Add current_balance to every account (default false)
include_shared boolean Include accounts shared with the user (default true)
{
  "success": true,
  "data": {
    "accounts": [
      {
        "id": "01a1029e-d31c-700e-bab1-474f5458ea23",
        "name": "My Checking",
        "type": "checking",
        "initial_balance": "1000.00",
        "currency": "USD",
        "icon": "account_balance",
        "color": "#4CAF50",
        "is_active": true,
        "include_in_totals": true,
        "closing_day": null,
        "due_day": null,
        "user_role": "owner",
        "is_owner": true,
        "is_shared": false,
        "current_balance": 5950
      }
    ],
    "owned_count": 2,
    "shared_count": 0,
    "total_count": 2
  }
}

Create account

POST /api/accounts
Field Type Required Rules
name string yes max 255
type string yes see the account object
initial_balance number yes
currency string yes exactly 3 characters
icon string no max 50
color string no max 50
is_active boolean no default true
include_in_totals boolean no default true
closing_day int no 1–31, kept only when type is credit_card
due_day int no 1–31, kept only when type is credit_card

201 returns the account, with the message "Account created successfully".

Get account

GET /api/accounts/{id}
Query Type Description
include_members boolean Add owner, member_count and members[] (id, name, email, role, joined_at)

Always includes current_balance, user_role and is_owner. Owners and shared users can read the account; anyone else gets 403.

Update account

PUT /api/accounts/{id}

Accepts the same fields as create, all optional. Changing type away from credit_card clears closing_day and due_day. Requires the owner or an editor of the account.

Delete account

DELETE /api/accounts/{id}

Owner only. Returns 422 "Cannot delete account that has transactions" if the account still has transactions.

Balances

GET /api/accounts/balances

Totals across the user's own active accounts that have include_in_totals = true. Shared accounts are not included.

{
  "success": true,
  "data": {
    "by_type": { "checking": { "USD": 5950 }, "savings": { "USD": 500 } },
    "by_currency": { "USD": 6450 }
  }
}

Categories

List categories

GET /api/categories
Query Description
type income or expense
{
  "success": true,
  "data": [
    { "id": "01a1029e-d322-736a-952e-9fd296e05e6d", "name": "Food", "type": "expense", "icon": "restaurant", "color": "#FF6384", "is_default": false }
  ]
}

Create category

POST /api/categories
Field Type Required Rules
name string yes max 255
type string yes income or expense
icon string no max 50
color string no max 50

201 returns the category, with the message "Category created successfully".

Get / update / delete

GET    /api/categories/{id}
PUT    /api/categories/{id}     (name, type, icon, color — all optional)
DELETE /api/categories/{id}

Only the owner can read or change a category (403 otherwise). Default categories (is_default: true) cannot be edited or deleted, and categories used by transactions cannot be deleted. Both cases return 422.


Transactions

Transaction object

Field Type Notes
id uuid
account_id / category_id uuid category_id may be null
type string income, expense, transfer
amount decimal always positive; type gives the direction
date date
time datetime|null the time of day is the meaningful part
description string|null
attachment string|null path on the public disk, served at https://ezfin.app/storage/{attachment}
transfer_transaction_id uuid|null the other leg of a transfer
is_recurring boolean created by a recurring transaction
account / category object embedded in list, show, create and update responses

List transactions

GET /api/transactions

Returns transactions from every account the user can access, including shared accounts.

Query Type Description
account_id uuid
category_id uuid
type string income, expense, transfer
start_date / end_date date Inclusive range on date
min_amount / max_amount number
search string Substring match on description
order_by string date (default), amount, description, created_at
order_dir string desc (default) or asc
per_page int default 25, max 100
page int

Returns a Laravel paginator of transactions, each with account and category embedded.

Create transaction

POST /api/transactions
Field Type Required Rules
account_id uuid yes an account the user owns or can edit
type string yes income, expense, transfer
amount number yes min 0.01
date date yes
time string no HH:mm
category_id uuid no must be one of the user's own categories
description string no max 255
attachment file no max 10 MB (send as multipart/form-data)
transfer_account_id uuid if type=transfer an account the user can edit, different from account_id

201 returns the transaction with account and category.

Transfers. Creating a transfer creates two linked transfer transactions: "Transfer to {destination}" on the source account and "Transfer from {source}" on the destination. Updating one leg keeps the amount, date and time of the other in sync, and deleting either leg deletes both.

Errors. 403 if the account, the category or the transfer destination is not one the user can use. 422 for validation errors.

Get transaction

GET /api/transactions/{id}

Includes account, category and transfer_transaction.account.

Update transaction

PUT /api/transactions/{id}

Accepts account_id, category_id, type, amount, date, time, description and attachment, all optional. To upload a new attachment, send POST /api/transactions/{id} as multipart/form-data with _method=PUT, because PHP does not parse multipart bodies on real PUT requests.

Delete transaction

DELETE /api/transactions/{id}

Deletes the attachment and, for transfers, the counterpart transaction as well.

Statistics

GET /api/transactions/statistics
Query Required Values
start_date yes date
end_date yes date ≥ start_date
group_by yes day, week, month, year, category
type no income, expense, both (default)

Transfers are excluded. Grouping by time returns chart-ready datasets (label formats: 2026-03-06, 2026-W10, 2026-03, 2026):

{
  "success": true,
  "data": {
    "labels": ["2026-01", "2026-02"],
    "datasets": [
      { "label": "Income", "data": [5000, 0], "backgroundColor": "#4CAF50" },
      { "label": "Expense", "data": [300, 200], "backgroundColor": "#F44336" }
    ]
  }
}

group_by=category returns:

{ "success": true, "data": { "income": [{ "label": "Salary", "value": "5000.00" }], "expense": [{ "label": "Food", "value": "500.00" }] } }

Recurring Transactions

List

GET /api/recurring-transactions
Query Description
account_id, category_id Filters
type income or expense
active true: only items with no end_date, or with an end_date today or later
order_by / order_dir Results are sorted by next_date (asc by default; order_dir=desc reverses)

Each item has next_date (the next occurrence, or null) and embeds account and category.

Create

POST /api/recurring-transactions
Field Type Required Rules
name string no max 255. Defaults to description, then to "Recurring Transaction"
account_id uuid yes one of the user's own accounts
category_id uuid no one of the user's own categories
type string yes income or expense
amount number yes min 0.01
description string no max 255
frequency string yes daily, weekly, bi_weekly, monthly, quarterly, semi_annually, annually
day_of_month int for monthly, quarterly, semi_annually and annually 1–31 (clamped to the length of each month)
day_of_week int for weekly and bi_weekly 0 (Sunday) – 6 (Saturday)
start_date date yes
end_date date no ≥ start_date
auto_generate boolean no default true. The nightly job creates the transactions automatically

201

{
  "success": true,
  "data": {
    "id": "01a1029e-d362-73d4-9588-0cf24830e970",
    "name": "Monthly Rent",
    "type": "expense",
    "amount": "1200.00",
    "frequency": "monthly",
    "day_of_month": 5,
    "day_of_week": null,
    "start_date": "2026-01-01T00:00:00.000000Z",
    "end_date": null,
    "auto_generate": true,
    "next_date": "2026-10-05T00:00:00.000000Z",
    "account": { },
    "category": { }
  },
  "message": "Recurring transaction created successfully"
}

Get / update / delete

GET    /api/recurring-transactions/{id}
PUT    /api/recurring-transactions/{id}
DELETE /api/recurring-transactions/{id}

Update accepts the same fields as create, all optional.

Generate a transaction now

POST /api/recurring-transactions/{id}/generate
Field Type Description
date date Optional. Defaults to the next occurrence

Creates a real transaction (is_recurring: true) and records it as last_generated.

{ "success": true, "data": { "transaction": { }, "recurring_transaction": { "next_date": "2026-11-05T00:00:00.000000Z" } }, "message": "Transaction generated successfully" }

400 "No valid occurrence date found" once the series has ended.

Upcoming occurrences

GET /api/recurring-transactions/upcoming?days=30

days ranges from 1 to 365 (default 30). Returns every occurrence within that window, sorted by date:

{
  "success": true,
  "data": [
    {
      "id": "01a1029e-d362-73d4-9588-0cf24830e970",
      "recurring_transaction_id": "01a1029e-d362-73d4-9588-0cf24830e970",
      "name": "Monthly Rent",
      "type": "expense",
      "amount": "1200.00",
      "description": null,
      "date": "2026-10-05",
      "auto_generate": true,
      "account": { },
      "category": { }
    }
  ]
}

Budgets

List

GET /api/budgets
Query Description
active true: start_date ≤ today and (end_date empty or ≥ today)
category_id Filter
period daily, weekly, monthly, quarterly, yearly, annually, custom

Every budget includes:

  • spent: the expenses in the current period (this day, week, month, quarter or year; for custom, the budget's own date range). Only the budget's category counts, or all expenses if it has no category.
  • progress: the percentage of amount spent, capped at 100.
{
  "success": true,
  "data": [
    {
      "id": "01a1029e-d378-7300-9c5e-6609e15fb1a6",
      "name": "Food Budget",
      "amount": "500.00",
      "period": "monthly",
      "start_date": "2026-01-01T00:00:00.000000Z",
      "end_date": null,
      "spent": 50,
      "progress": 10,
      "category": { }
    }
  ]
}

Create

POST /api/budgets
Field Type Required Rules
name string yes max 255
amount number yes min 0
period string yes see List
start_date date yes
end_date date no ≥ start_date
category_id uuid no one of the user's own categories

Get / update / delete

GET    /api/budgets/{id}        (includes spent and progress)
PUT    /api/budgets/{id}
DELETE /api/budgets/{id}

Progress of active budgets

GET /api/budgets/progress

Every currently active budget, with spent and progress.


Financial Goals

Both prefixes are equivalent: /api/goals and /api/financial-goals.

List

GET /api/goals
Query Description
is_completed true or false
order_by name, target_date, target_amount, current_amount, created_at (default)
order_dir desc (default) or asc

Every goal includes progress (0–100). Goals with a target_date also include days_remaining (whole days; negative when past) and is_overdue.

{
  "success": true,
  "data": [
    {
      "id": "01a1029e-d38a-704f-8341-5caf85b1749e",
      "name": "Emergency Fund",
      "target_amount": "10000.00",
      "current_amount": "6500.00",
      "target_date": "2027-10-03T00:00:00.000000Z",
      "is_completed": false,
      "progress": 65,
      "days_remaining": 364,
      "is_overdue": false
    }
  ]
}

Create

POST /api/goals
Field Type Required Rules
name string yes max 255
target_amount number yes min 0.01
current_amount number no min 0, default 0
target_date date no today or later
description string no
icon / color string no max 50

A goal whose current_amount already reaches target_amount is marked is_completed: true.

Get / update / delete

GET    /api/goals/{id}
PUT    /api/goals/{id}          (same fields as create, plus is_completed)
DELETE /api/goals/{id}

Add funds

POST /api/goals/{id}/add-funds
Field Type Rules
amount number min 0.01

Returns the goal with the updated current_amount, progress and is_completed, and the message "Funds added to goal successfully".


Customers & Suppliers

Customers (/api/customers) and suppliers (/api/suppliers) have identical fields.

Field Type Rules
name string required on create, max 255
document string CPF/CNPJ/tax ID, max 20
email string valid e-mail
phone string max 20
address string
city / state / country string max 100
postal_code string max 20
notes string
is_active boolean default true
GET    /api/customers            ?active=true|false  ?search=  (name, e-mail or document)
POST   /api/customers
GET    /api/customers/{id}
PUT    /api/customers/{id}
DELETE /api/customers/{id}

The /api/suppliers routes are the same.

  • List. Customers come with total_receivable, overdue_receivable and their accounts_receivable. Suppliers come with total_payable, overdue_payable and their accounts_payable.
  • Show. Also embeds the payments of each receivable or payable.
  • Delete. 422 while the customer still has unpaid receivables, or the supplier unpaid payables.

Accounts Receivable & Payable

Receivables (/api/accounts-receivable, linked to a customer_id) and payables (/api/accounts-payable, linked to a supplier_id) work the same way.

Object

Field Type Notes
customer_id / supplier_id uuid
invoice_number string|null
description string
amount decimal
paid_amount / remaining_amount decimal maintained by the API
issue_date / due_date date
status string pending, partial, paid, overdue, computed automatically
notes string|null

Endpoints

GET    /api/accounts-receivable            paginated
POST   /api/accounts-receivable
GET    /api/accounts-receivable/summary
GET    /api/accounts-receivable/{id}       includes customer and payments (with their account)
PUT    /api/accounts-receivable/{id}
DELETE /api/accounts-receivable/{id}
POST   /api/accounts-receivable/{id}/payments

The /api/accounts-payable routes are the same, with supplier_id.

List filters: status, customer_id (or supplier_id), start_date/end_date (on due_date), per_page (default 25). Results are sorted by due_date ascending.

Create / update fields

Field Type Required Rules
customer_id / supplier_id uuid yes must belong to the user
description string yes max 255
amount number yes min 0.01
issue_date date yes
due_date date yes ≥ issue_date
invoice_number string no max 100
notes string no

paid_amount, remaining_amount, status and user_id cannot be set by the client. Changing amount recalculates remaining_amount and status. Deleting a receivable or payable that already has payments returns 422.

Summary

{
  "success": true,
  "data": {
    "total": 5000,
    "pending": 0,
    "overdue": 0,
    "partial": 3000,
    "paid": 2000,
    "count": 1,
    "pending_count": 0,
    "overdue_count": 0,
    "paid_count": 0
  }
}

In the summary, total is the sum of amount; pending, overdue and partial are sums of remaining_amount; paid is the sum of paid_amount.

Register a payment

POST /api/accounts-receivable/{id}/payments
POST /api/accounts-payable/{id}/payments
Field Type Required Rules
amount number yes min 0.01, max = the current remaining_amount
account_id uuid yes one of the user's own bank accounts
description string no max 255, default "Payment for {invoice_number or description}"
payment_date date no default today

Every payment automatically creates a matching transaction on account_id: income for receivables, expense for payables. It also updates paid_amount, remaining_amount and status.

201

{
  "success": true,
  "data": {
    "id": "01a1029e-d3a7-70a2-ae5f-7749effe04a6",
    "type": "receivable",
    "amount": "2000.00",
    "payment_date": "2026-10-03T00:00:00.000000Z",
    "description": "Payment for INV-001",
    "transaction_id": "01a1029e-d3a8-7177-b409-5401ad4ebdec",
    "account": { },
    "receivable": { "status": "partial", "paid_amount": "2000.00", "remaining_amount": "3000.00", "customer": { } }
  },
  "message": "Payment received successfully."
}

For payables, the response has payable (with supplier) instead of receivable, and the message is "Payment made successfully.".


Payments

GET    /api/payments
GET    /api/payments/{id}
PUT    /api/payments/{id}
DELETE /api/payments/{id}

List filters: type (receivable or payable), account_id, date_from, date_to, per_page (default 25). Results are sorted by payment_date descending, and each payment embeds account, receivable.customer and payable.supplier.

Show also embeds the linked transaction.

Update accepts amount (max = remaining_amount + this payment's amount), account_id (one of the user's own accounts), description and payment_date. The linked transaction and the receivable or payable totals are updated to match.

Delete reverses the paid amount on the receivable or payable, recalculates its status, and deletes the linked transaction.


Reports

All report endpoints take these common query parameters:

Query Values
period daily (last 30 days, grouped by day), weekly (12 weeks), monthly (12 months, the default), yearly (5 years)
start_date / end_date Override the range set by period
accounts[] Restrict to some of the user's own account IDs, e.g. ?accounts[]=uuid1&accounts[]=uuid2

Reports read the user's own transactions. Transfers never count as income or expense.

Income vs expense

GET /api/reports/income-expense

Takes an extra group_by parameter (day, week, month or year) that overrides the grouping chosen by period.

{
  "success": true,
  "data": {
    "labels": ["2026-09", "2026-10"],
    "datasets": [
      { "label": "Income", "data": [5000, 5200], "backgroundColor": "#4CAF50" },
      { "label": "Expense", "data": [300, 350], "backgroundColor": "#F44336" }
    ],
    "summary": { "total_income": 10200, "total_expense": 650, "net_amount": 9550 },
    "period": { "start": "2025-10-03", "end": "2026-10-03", "group_by": "month" }
  }
}

By category

GET /api/reports/category

Takes an extra type parameter: expense (default) or income.

{
  "success": true,
  "data": {
    "labels": ["Food", "Transport"],
    "datasets": [{ "data": [650, 120], "backgroundColor": ["#FF6384", "#36A2EB"] }],
    "details": [
      { "category": "Food", "icon": "restaurant", "color": "#FF6384", "amount": 650, "count": 3, "percentage": 84.4 }
    ],
    "summary": { "total": 770, "transaction_count": 5, "category_count": 2 },
    "period": { "start": "2025-10-03", "end": "2026-10-03", "type": "expense" }
  }
}

Cash flow

GET /api/reports/cash-flow
{
  "success": true,
  "data": {
    "labels": ["Sep 2026", "Oct 2026"],
    "datasets": [
      { "label": "Balance", "data": [4700, 9550], "type": "line", "backgroundColor": "#2196F3", "borderColor": "#2196F3" },
      { "label": "Income", "data": [5000, 5200], "backgroundColor": "#4CAF50" },
      { "label": "Expense", "data": [300, 350], "backgroundColor": "#F44336" }
    ],
    "summary": { "initial_balance": 0, "final_balance": 9550, "total_income": 10200, "total_expense": 650, "net_change": 9550 },
    "period": { "start": "2026-09-01", "end": "2026-10-03" }
  }
}

initial_balance is the net of income and expense before start_date. Data points are grouped by the granularity of period.

Net worth

GET /api/reports/net-worth

Takes period, start_date and end_date. Account balances follow the same rule as current_balance (initial balance + income − expense, as of each date).

  • Assets: checking, savings, investment and cash accounts.
  • Liabilities: credit_card accounts, counted by the absolute value of their balance.
{
  "success": true,
  "data": {
    "labels": ["Aug 2026", "Sep 2026", "Oct 2026"],
    "datasets": [
      { "label": "Assets", "data": [1000, 5700, 10700] },
      { "label": "Liabilities", "data": [0, 0, 150] },
      { "label": "Net Worth", "data": [1000, 5700, 10550], "type": "line" }
    ],
    "summary": { "current_assets": 10700, "current_liabilities": 150, "current_net_worth": 10550, "change": 9550, "change_percentage": 955 },
    "accounts": [
      { "id": "…", "name": "Checking", "type": "checking", "balance": 10700, "is_asset": true },
      { "id": "…", "name": "Card", "type": "credit_card", "balance": -150, "is_asset": false }
    ],
    "period": { "start": "2026-08-03", "end": "2026-10-03" }
  }
}

Saved reports

POST   /api/reports/save
GET    /api/reports/saved           paginated, 10 per page
GET    /api/reports/saved/{id}      runs the report with its saved parameters
DELETE /api/reports/saved/{id}

Save body

Field Type Required Rules
name string yes max 255
type string yes income_expense, category, cash_flow, net_worth
parameters object yes the query parameters of that report, e.g. {"period": "monthly"}; may be empty

201

{ "success": true, "data": { "id": "…", "name": "Monthly overview", "type": "income_expense", "parameters": { "period": "monthly" } }, "message": "Report saved successfully" }

GET /reports/saved/{id} returns exactly the same response as calling that report endpoint with the saved parameters. Reports saved from the website appear here too.


Settings

Get settings

GET /api/settings

Creates default settings if the user has none yet.

{
  "success": true,
  "data": {
    "language": "en",
    "currency": "USD",
    "date_format": "Y-m-d",
    "time_format": "H:i",
    "theme": "light",
    "notifications_enabled": true,
    "notification_preferences": null,
    "device_token": null,
    "device_type": null
  }
}

Update settings

PUT /api/settings
Field Rules
language en, pt-br, es. Also updates the user's language
currency 3 characters
date_format Y-m-d, d/m/Y, m/d/Y, d-m-Y, m-d-Y
time_format H:i, h:i A
theme light, dark, system
notifications_enabled boolean
notification_preferences a JSON-encoded string, e.g. "{\"email\":false}"

Update theme

PATCH /api/settings/theme

Body: {"theme": "dark"}. Returns {"success": true, "data": {"theme": "dark"}, "message": "Theme updated successfully"}.


Dashboard

Overview

GET /api/dashboard
{
  "success": true,
  "data": {
    "summary": {
      "total_balance": 10550,
      "formatted_balance": "$10,550.00",
      "monthly_income": 5200,
      "formatted_income": "$5,200.00",
      "monthly_expense": 350,
      "formatted_expense": "$350.00",
      "net_flow": 4850,
      "formatted_net_flow": "$4,850.00",
      "income_change": 4,
      "expense_change": 16.67
    },
    "accounts": [
      { "id": "…", "name": "Checking", "type": "checking", "balance": 10700, "currency": "USD", "formatted_balance": "$10,700.00" }
    ],
    "budgets": [
      { "id": "…", "name": "Food", "amount": 1000, "category": "Food", "budget": "$1,000.00", "spent": 350, "remaining": 650, "percentage": 35 }
    ],
    "goals": [
      { "id": "…", "name": "Trip", "target_amount": "1000.00", "current_amount": "250.00", "progress": 25, "target_date": null }
    ],
    "recent_transactions": [
      { "id": "…", "type": "expense", "amount": "200.00", "formatted_amount": "$200.00", "description": "Groceries", "date": "2026-10-01", "category": "Food", "account": "Checking" }
    ]
  }
}
  • total_balance is the sum of current_balance over the user's active, include_in_totals accounts. accounts lists every account the user owns.
  • Monthly figures cover the current calendar month across all accessible accounts. income_change and expense_change are percentages against the previous month.
  • budgets (up to 5) shows active budgets with this month's spending. goals (up to 3) shows incomplete goals.
  • recent_transactions holds the last 10 transactions.
  • The formatted_* fields always use a $ symbol. Format the numeric fields yourself for other currencies.

Widgets

GET /api/dashboard/widgets
{
  "success": true,
  "data": {
    "counts": { "accounts": 2, "categories": 2, "transactions": 5, "budgets": 1, "goals": 1 },
    "category_spending": [{ "category": "Food", "amount": 350, "count": 2 }]
  }
}

category_spending holds the top 5 expense categories of the current month.

Recent activity

GET /api/dashboard/recent-activity

The 15 most recent transactions, as {type: "transaction", action, description, amount, date, data: {account, category}}.


Notifications

In-app notifications sent to the user.

List

GET /api/notifications
Query Description
status pending, sent, delivered, read, failed
channel in_app, email, sms, whatsapp
unread_only boolean
per_page default 15

Fetching the list marks the returned in_app notifications as delivered.

{
  "success": true,
  "data": {
    "notifications": [
      {
        "id": "01a1029e-d3fc-73ca-972e-f6e260d15387",
        "title": "Budget Alert",
        "content": "Your Food budget is at 90%",
        "channel": "in_app",
        "status": "sent",
        "read_at": null,
        "sent_at": "2026-10-03T16:35:25.000000Z",
        "delivered_at": null,
        "created_at": "2026-10-03T16:35:25.000000Z",
        "is_read": false
      }
    ],
    "pagination": { "current_page": 1, "per_page": 15, "total": 1, "last_page": 1 },
    "summary": { "total": 1, "unread": 1, "in_app": 1, "email": 0 }
  }
}

Actions

POST   /api/notifications/{id}/read     mark one as read
POST   /api/notifications/read-all      → {"message": "Marked 3 notifications as read"}
DELETE /api/notifications/{id}

A notification that belongs to another user returns 404.


Mobile

Sync

GET /api/mobile/sync?last_sync=2026-03-06T15:00:00Z

Without last_sync, returns everything. With it, returns only records whose updated_at is later than last_sync. last_sync must be a valid date (422 otherwise).

{
  "success": true,
  "data": {
    "accounts": [ ],
    "shared_accounts": [ ],
    "categories": [ ],
    "transactions": [ ],
    "budgets": [ ],
    "goals": [ ],
    "sync_timestamp": "2026-10-03T16:35:25.000000Z"
  }
}

Each account embeds its transactions, and each shared account adds user_role and is_shared. Store sync_timestamp and send it as last_sync next time.

Deleted records are not reported by an incremental sync, so run a full sync (no last_sync) periodically to drop items removed elsewhere.

Offline bundle

GET /api/mobile/offline-data

Returns the essentials for offline use: user (id, name, email, preferred_currency, language), accounts, shared_accounts, categories, the 100 most recent transactions in recent_transactions, currencies (a map of code to symbol, e.g. "BRL": "R$") and downloaded_at.

Register device token

POST /api/mobile/device-token
Field Rules
device_token string, max 255 (FCM/APNs token)
device_type ios, android, web
{ "success": true, "message": "Device token updated successfully.", "data": { "device_token": "fcm_token_here", "device_type": "android" } }

Admin API

Every endpoint is under /api/admin and requires a token belonging to a user with is_admin = true. Anyone else receives 403 {"message": "Unauthorized action."}. Validation errors here use the {message, errors} shape.

Analytics endpoints accept period: 7days, 30days (default), 90days or 1year.

Dashboard

GET /api/admin/dashboard
GET /api/admin/dashboard/stats?period=30days
GET /api/admin/dashboard/charts
  • dashboard returns metrics.users (total, active = users with transactions in the last 30 days, new_today, new_this_week, new_this_month, growth_rate), metrics.transactions (total, today, this_month, total_revenue, total_expenses, net_flow), metrics.whatsapp, metrics.subscriptions (active, revenue) and recent_activity (the latest 10 users and transactions).
  • stats returns user_growth[] (date, count), transaction_volume[] (date, count, income, expenses), category_usage[] (top 10) and period.
  • charts returns monthly_revenue[] (the last 12 months), subscription_distribution[] (Free first) and transaction_types[] for the current month.

Users

GET    /api/admin/users
POST   /api/admin/users
GET    /api/admin/users/{id}
PUT    /api/admin/users/{id}
DELETE /api/admin/users/{id}
POST   /api/admin/users/{id}/toggle-admin
POST   /api/admin/users/{id}/suspend
POST   /api/admin/users/{id}/activate

List filters: search (name or e-mail), is_admin, subscription_status (active or free), suspended (boolean), registered_after, registered_before, sort_by (name, email, created_at (default), updated_at, is_admin, language, preferred_currency), sort_direction, per_page (default 15).

Each user in the list has is_suspended, suspended_at, subscription and stats (accounts_count, transactions_count, total_balance). The list comes back as data.users[] plus data.pagination.

Show returns user, subscription, statistics (total_balance, monthly_income, monthly_expense, net_flow, and the number of accounts, categories, transactions, budgets and goals), accounts[], recent_transactions[] and notification_logs[].

Create (201, returns data.user)

Field Required Rules
name yes max 255
email yes unique
password yes min 8
phone no max 20
language no default en
preferred_currency no default USD
is_admin no boolean

Update accepts the same fields, all optional.

Rules and side effects

  • The last remaining admin cannot be demoted (update, toggle-admin) or deleted. These attempts return 400.
  • suspend sets suspended_at and revokes every token of that user. A suspended user cannot log in (403) and is refused on every endpoint.
  • activate clears the suspension.
  • Admins cannot suspend themselves.
{ "success": true, "message": "User suspended", "data": { "user_id": "…", "is_suspended": true, "suspended_at": "2026-10-03T16:35:25.000000Z" } }

Transactions

GET    /api/admin/transactions
GET    /api/admin/transactions/analytics?period=30days
GET    /api/admin/transactions/{id}
DELETE /api/admin/transactions/{id}

List filters: search (description, user name or user e-mail), type, user_id, category_id, min_amount, max_amount, date_from, date_to, sort_by (date (default), amount, type, description, created_at), sort_direction, per_page.

The list returns transactions[] (each with user, account and category), pagination, and a global summary (total_income, total_expense, total_transactions).

analytics returns transaction_volume, top_categories, top_users, type_distribution, average_amounts, period and date_range.

Deleting a transfer leg also deletes its counterpart.

Categories

GET    /api/admin/categories
GET    /api/admin/categories/analytics?period=30days
GET    /api/admin/categories/{id}
POST   /api/admin/categories
PUT    /api/admin/categories/{id}
DELETE /api/admin/categories/{id}
POST   /api/admin/categories/bulk-actions

List filters: search, user_id, type (income or expense), usage (used or unused), per_page. Returns categories[] (each with user and transactions_count), pagination, and summary (total_categories, income_categories, expense_categories, used_categories, unused_categories).

Show returns category, statistics (total_transactions, total_amount, income_amount, expense_amount, average_amount, unique_users, first_used, last_used) and recent_transactions[].

Create takes name, type (income or expense), user_id (the owner) and, optionally, color (max 7) and icon (max 50). Update takes the same fields, all optional.

Delete returns 400 if the category has transactions.

Bulk actions

Field Required Rules
action yes delete (refused with 400 if any category has transactions) or merge
category_ids yes array of category IDs
target_category_id for merge the category that receives the transactions. The source categories are deleted.

Notification templates

GET    /api/admin/notifications
GET    /api/admin/notifications/analytics?period=30days
GET    /api/admin/notifications/{id}
POST   /api/admin/notifications
PUT    /api/admin/notifications/{id}
DELETE /api/admin/notifications/{id}
POST   /api/admin/notifications/{id}/send

List filters: search, type, is_active, per_page. Returns templates[], pagination and summary.

Create / update body (on update, every required field must be sent again)

Field Required Rules
name yes max 255. The slug is generated from it
type yes email, sms, whatsapp, in_app
content yes supports {{user_name}}
target_audience yes all, admin, subscribed, new_users
subject no max 255
trigger_event no max 255
variables no array
is_active no boolean, default true
category no max 100
settings no object

Send

Field Description
user_ids array of user IDs
send_to_all boolean. Sends to every user

Send creates one pending notification log per user, with the content rendered for that user, and returns {"template_id": "…", "users_count": 4}. For in_app templates, the user sees the notification in GET /api/notifications.

Show returns template, statistics (sent, delivered, read, failed, delivery rate) and recent_logs[].

Analytics returns notification_volume, delivery_rates (per channel) and top_templates.

WhatsApp connections

GET    /api/admin/whatsapp
POST   /api/admin/whatsapp
GET    /api/admin/whatsapp/{id}
PUT    /api/admin/whatsapp/{id}
DELETE /api/admin/whatsapp/{id}
GET    /api/admin/whatsapp/{id}/messages
POST   /api/admin/whatsapp/{id}/test
POST   /api/admin/whatsapp/messages/{message_id}/process

Create takes name, phone_number (max 20), business_account_id, access_token, webhook_verify_token and, optionally, settings. New connections start with status: "inactive". Update requires the same fields plus status (active, inactive or disconnected). Secrets (access_token, webhook_verify_token) are never returned.

List filters by search (name or phone) and status. Show returns connection, statistics and the 15 most recent messages.

Messages filters by status, direction (inbound or outbound), date_from, date_to, search and per_page.

test marks the connection as active and stamps last_connected_at. process marks a pending message as processed, and returns 400 if the message is not pending.


Endpoint Index

Method Path Auth
GET /test —
POST /register —
POST /login —
POST /logout user
GET /user · /profile user
POST /refresh user
GET · POST /tokens user
DELETE /tokens/{id} user
PUT /profile user
POST /profile/password · /profile/avatar · /profile/delete-account user
GET · POST /accounts user
GET /accounts/balances user
GET · PUT · DELETE /accounts/{id} user
GET · POST /categories user
GET · PUT · DELETE /categories/{id} user
GET · POST /transactions user
GET /transactions/statistics user
GET · PUT · DELETE /transactions/{id} user
GET · POST /recurring-transactions user
GET /recurring-transactions/upcoming user
GET · PUT · DELETE /recurring-transactions/{id} user
POST /recurring-transactions/{id}/generate user
GET · POST /budgets user
GET /budgets/progress user
GET · PUT · DELETE /budgets/{id} user
GET · POST /goals · /financial-goals user
GET · PUT · DELETE /goals/{id} · /financial-goals/{id} user
POST /goals/{id}/add-funds · /financial-goals/{id}/add-funds user
GET · POST /customers · /suppliers user
GET · PUT · DELETE /customers/{id} · /suppliers/{id} user
GET · POST /accounts-receivable · /accounts-payable user
GET /accounts-receivable/summary · /accounts-payable/summary user
GET · PUT · DELETE /accounts-receivable/{id} · /accounts-payable/{id} user
POST /accounts-receivable/{id}/payments · /accounts-payable/{id}/payments user
GET /payments user
GET · PUT · DELETE /payments/{id} user
GET /reports/income-expense · /reports/category · /reports/cash-flow · /reports/net-worth user
POST /reports/save user
GET /reports/saved user
GET · DELETE /reports/saved/{id} user
GET · PUT /settings user
PATCH /settings/theme user
GET /dashboard · /dashboard/widgets · /dashboard/recent-activity user
GET /notifications user
POST /notifications/{id}/read · /notifications/read-all user
DELETE /notifications/{id} user
GET /mobile/sync · /mobile/offline-data user
POST /mobile/device-token user
GET /admin/dashboard · /admin/dashboard/stats · /admin/dashboard/charts admin
GET · POST /admin/users admin
GET · PUT · DELETE /admin/users/{id} admin
POST /admin/users/{id}/toggle-admin · /suspend · /activate admin
GET /admin/transactions · /admin/transactions/analytics admin
GET · DELETE /admin/transactions/{id} admin
GET · POST /admin/categories admin
GET /admin/categories/analytics admin
GET · PUT · DELETE /admin/categories/{id} admin
POST /admin/categories/bulk-actions admin
GET · POST /admin/notifications admin
GET /admin/notifications/analytics admin
GET · PUT · DELETE /admin/notifications/{id} admin
POST /admin/notifications/{id}/send admin
GET · POST /admin/whatsapp admin
GET · PUT · DELETE /admin/whatsapp/{id} admin
GET /admin/whatsapp/{id}/messages admin
POST /admin/whatsapp/{id}/test admin
POST /admin/whatsapp/messages/{id}/process admin

PUT endpoints also accept PATCH, except PUT /profile and PUT /settings.


Quick Start

# 1. Log in and keep the token
TOKEN=$(curl -s -X POST https://ezfin.app/api/login \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"email":"john@example.com","password":"secret123","device_name":"My App"}' \
  | jq -r '.data.access_token')

# 2. Find an account
ACCOUNT=$(curl -s https://ezfin.app/api/accounts \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
  | jq -r '.data.accounts[0].id')

# 3. Record an expense
curl -X POST https://ezfin.app/api/transactions \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d "{\"account_id\":\"$ACCOUNT\",\"type\":\"expense\",\"amount\":42.50,\"date\":\"2026-03-06\",\"description\":\"Groceries\"}"

# 4. Dashboard
curl https://ezfin.app/api/dashboard -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"

# 5. Incremental sync for a mobile app
curl "https://ezfin.app/api/mobile/sync?last_sync=2026-03-06T15:00:00Z" \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"

JavaScript:

const api = async (path, { method = 'GET', body, token } = {}) => {
  const res = await fetch(`https://ezfin.app/api${path}`, {
    method,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
    },
    body: body && JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.message ?? 'Request failed'), { status: res.status, errors: json.errors });
  return json.data ?? json;
};

const { access_token } = await api('/login', { method: 'POST', body: { email, password, device_name: 'Web' } });
const dashboard = await api('/dashboard', { token: access_token });
Need help with the API? Write to contato@ezfin.app or see the product documentation.