Yaar Khata API Reference
REST API for the Yaar Khata expense-splitting platform. It powers the Flutter mobile app and any third-party client.
- Base URL:
https://yaarkhata.com/api/v1 - Auth: Laravel Sanctum bearer tokens
- Content type:
application/json(sendAccept: application/jsonon every request) - Encoding: all JSON keys are camelCase; money is a JSON number (
DECIMALserver-side); timestamps are ISO-8601 strings (e.g.2026-07-05T20:14:00+00:00).
Table of contents
- Authentication
- Conventions
- Errors
- Auth & profile endpoints
- Content endpoints (public)
- Groups
- Expenses
- Settlements
- Group data (activities, balances, settle-up)
- Categories
- Coupons
- Support tickets
- Real-time (Reverb / WebSockets)
- Health check
1. Authentication
Yaar Khata uses Sanctum personal access tokens.
- Call
POST /registerorPOST /login. The response contains atoken. - Send that token on every authenticated request:
Authorization: Bearer 1|AbCdEf0123...
Accept: application/json
Tokens are issued per device (device_name, default "mobile"). POST /logout
revokes only the token used for that call.
2. Conventions
| Topic | Detail |
|---|---|
| Versioning | All routes are under /api/v1. |
| IDs | Users/members use numeric string ids ("42"). Groups, expenses, settlements and tickets are addressed by an opaque ULID publicId (e.g. 01J8Z...), never their DB id. |
| Pagination | List endpoints return Laravel paginator JSON: a data array plus links and meta (current_page, last_page, per_page, total). Page size is fixed per resource (groups 50, expenses/settlements/activities 100, tickets 30). Request a page with ?page=2. |
| Single objects | Wrapped in { "data": { ... } }. |
| Rate limiting | register & login: 10 req/min per IP. forgot-password: 5 req/min per IP. Exceeding returns 429. |
3. Errors
Errors are JSON with the correct HTTP status.
| Status | Meaning |
|---|---|
401 |
Missing/invalid token. |
403 |
Authenticated but not allowed (e.g. not a member of the group, or not the group owner). |
404 |
Resource not found (or a publicId you can't see). |
422 |
Validation failed. |
429 |
Rate limit exceeded. |
Validation error shape (422):
{
"message": "The email has already been taken.",
"errors": {
"email": ["The email has already been taken."]
}
}
4. Auth & profile endpoints
POST /register — public
Create an account and receive a token.
Body
| Field | Type | Rules |
|---|---|---|
name |
string | required, max 255 |
email |
string | required, email, unique |
password |
string | required, min 8 |
device_name |
string | optional (default mobile) |
201 Created
{
"token": "3|k9f...",
"data": {
"id": "1", "name": "Ada Lovelace", "email": "ada@example.com",
"phone": null, "currencyCode": "PKR", "avatarInitial": "A",
"avatarColor": 0, "isPremium": false
}
}
POST /login — public
Body: email (required), password (required), device_name (optional).
Returns the same 201 payload as register. Wrong credentials → 422.
POST /forgot-password — public
Body: email (required). Always returns 200 with a generic message (no user enumeration).
{ "message": "If that account exists, a reset link has been sent." }
POST /logout — auth
Revokes the current token. 200 → { "message": "Logged out." }
GET /user — auth
Returns the current user in a data envelope (same user object shape as register).
PUT /user — auth
Update the profile. All fields optional; only sent fields change.
| Field | Type | Rules |
|---|---|---|
name |
string | max 255 (also refreshes avatarInitial) |
email |
string | email, unique (except self) |
phone |
string | max 32, nullable |
currencyCode |
string | max 8 |
avatarColor |
int | 0–20 |
Returns the updated user in a data envelope.
5. Content endpoints (public)
These are cached for 5 minutes and require no auth.
GET /plans
Subscription plans (active only, ordered).
{
"data": [{
"id": "2", "name": "Pro Monthly", "slug": "pro-monthly",
"description": null, "price": 499, "currency": "PKR",
"interval": "month", "features": ["Unlimited groups", "Receipt storage"],
"trialDays": 0, "isFeatured": true
}]
}
GET /faqs
{ "data": [{ "id": "1", "question": "…", "answer": "…", "category": "Basics" }] }
GET /settings
Public app settings & feature flags (drives remote config in the app).
{
"data": {
"app_name": "Yaar Khata",
"support_email": "support@yaarkhata.com",
"maintenance_mode": false,
"min_supported_version": "1.0.0",
"force_update": false,
"feature_charts": true,
"feature_receipts": true,
"promo_banner": ""
}
}
6. Groups
All group routes are auth. A caller must be the owner or a member; otherwise 403.
Groups are addressed by publicId.
GET /groups
Paginated list (50/page) of groups you own or belong to, newest first. Each item:
{
"id": "01J8Z…", "name": "Trip to Hunza", "colorKey": "green",
"currencyCode": "PKR", "coverIcon": "group",
"members": [
{ "id": "1", "name": "Ada", "avatarInitial": "A", "avatarColor": 0, "userId": "1" }
],
"createdAt": "2026-07-05T20:00:00+00:00"
}
POST /groups
Create a group. The owner is added as the first member automatically.
| Field | Type | Rules |
|---|---|---|
name |
string | required, max 255 |
colorKey |
string | optional (default green) |
currencyCode |
string | optional (default PKR) |
coverIcon |
string | optional (default group) |
members |
array | optional list of extra members |
members[].name |
string | required |
members[].avatarColor |
int | optional |
Returns the created group (201, with members).
GET /groups/{publicId}
Single group with members.
PUT /groups/{publicId}
Partial update. Optional: name, colorKey, currencyCode, and members (array).
When members is sent it reconciles the roster: items with an id are updated,
items without are created, and any existing member not in the list is removed.
DELETE /groups/{publicId}
Owner only (others get 403). 200 → { "message": "Group deleted." }
7. Expenses
Auth. Caller must belong to the group. Expenses are addressed by publicId.
The type field covers expense, income, transfer, and fromBill.
Writes broadcast a real-time GroupDataChanged event (see §13).
GET /groups/{groupPublicId}/expenses
Paginated (100/page), newest first by date. Each item includes its splits:
{
"id": "01J8Z…", "groupId": "01J8Y…", "type": "expense",
"title": "Dinner", "categoryIcon": "restaurant", "amount": 3200,
"paidByMemberId": "1", "date": "2026-07-05T19:00:00+00:00",
"note": null, "receiptPath": null,
"splits": [
{ "memberId": "1", "mode": "equally", "value": 0, "included": true, "computedAmount": 1600 },
{ "memberId": "2", "mode": "equally", "value": 0, "included": true, "computedAmount": 1600 }
],
"fromMemberId": null, "toMemberId": null,
"recurrence": "none", "recurrenceDay": null,
"recurrenceInterval": null, "recurrenceUnit": null, "nextDue": null
}
POST /groups/{groupPublicId}/expenses
Create an expense.
| Field | Type | Rules |
|---|---|---|
type |
string | expense|income|transfer|fromBill (default expense) |
title |
string | required, max 255 |
amount |
number | required, ≥ 0 |
categoryIcon |
string | optional |
paidByMemberId |
int | optional (payer, for expenses) |
date |
date | optional (default now) |
note |
string | optional |
receiptPath |
string | optional (uploaded receipt reference) |
fromMemberId / toMemberId |
int | optional (for transfer) |
recurrence |
string | none|daily|weekly|monthly|yearly|custom |
recurrenceDay |
int | optional, 1–28 (monthly) |
recurrenceInterval |
int | optional, ≥ 1 (custom) |
recurrenceUnit |
string | days|weeks|months (custom) |
splits |
array | per-member split rows |
splits[].memberId |
int | required |
splits[].mode |
string | equally|amounts|shares (default equally) |
splits[].value |
number | weight/amount for the mode |
splits[].included |
bool | default true |
splits[].computedAmount |
number | resolved amount this member owes |
If recurrence != none, the server computes and stores nextDue; the hourly
yaarkhata:materialize-recurring job then generates future occurrences.
Returns the created expense (with splits).
PUT /expenses/{publicId}
Same fields as create (all become optional except title/amount which stay
required by the shared validator). Sending splits replaces all existing splits.
DELETE /expenses/{publicId}
200 → { "message": "Expense deleted." }
8. Settlements
Auth + group membership. Records a payment from one member to another.
GET /groups/{groupPublicId}/settlements
Paginated (100/page), newest first.
{
"id": "01J8Z…", "groupId": "01J8Y…",
"fromMemberId": "2", "toMemberId": "1",
"amount": 1600, "date": "2026-07-05T20:10:00+00:00", "status": "paid"
}
POST /groups/{groupPublicId}/settlements
| Field | Type | Rules |
|---|---|---|
fromMemberId |
int | required |
toMemberId |
int | required, must differ from fromMemberId |
amount |
number | required, ≥ 0 |
date |
date | optional (default now) |
Creates the settlement (status: "paid"), logs an activity, and broadcasts.
9. Group data
Auth + group membership. Derived, server-authoritative data.
GET /groups/{groupPublicId}/activities
Paginated (100/page) activity feed:
{
"id": "12", "groupId": "01J8Y…", "type": "expense",
"actorMemberId": "1", "refId": "01J8Z…",
"summary": "Added Dinner", "amount": 3200,
"timestamp": "2026-07-05T19:00:00+00:00"
}
GET /groups/{groupPublicId}/balances
Net balance per member (positive = owed to them, negative = they owe).
Computed by App\Services\BalanceCalculator.
{ "data": [ { "memberId": "1", "net": 1600 }, { "memberId": "2", "net": -1600 } ] }
GET /groups/{groupPublicId}/suggested-payments
Minimal set of payments to settle everyone (debt simplification):
{ "data": [ { "fromId": "2", "toId": "1", "amount": 1600 } ] }
10. Categories
Auth. Returns the 14 default categories plus the caller's custom ones.
GET /categories
{
"data": [
{ "id": "3", "key": "food", "label": "Food", "icon": "restaurant", "tint": "#FFC44D", "isDefault": true }
]
}
POST /categories
Create/replace a custom category (keyed by slug of label).
| Field | Type | Rules |
|---|---|---|
label |
string | required, max 40 |
icon |
string | optional (default sell) |
tint |
string | optional, max 16 |
201 → { "data": { "id": "…", "key": "…", "label": "…" } }
DELETE /categories/{category}
Deletes one of your own custom categories (else 403).
11. Coupons
POST /coupons/validate — auth
Validate a discount code before checkout.
| Field | Type | Rules |
|---|---|---|
code |
string | required |
planId |
int | optional (validates plan restriction & computes discount) |
Valid (200)
{ "valid": true, "type": "percent", "value": 20, "discount": 99.8, "message": "Coupon applied." }
Invalid (422)
{ "valid": false, "message": "This coupon is not valid." }
12. Support tickets
Auth. Users see only their own tickets.
GET /tickets
Paginated (30/page) list. Ticket object:
{
"id": "01J8Z…", "subject": "Can't add member", "category": "bug",
"status": "open", "priority": "normal",
"lastReplyAt": "2026-07-05T20:00:00+00:00",
"createdAt": "2026-07-05T19:55:00+00:00"
}
POST /tickets
| Field | Type | Rules |
|---|---|---|
subject |
string | required, max 255 |
message |
string | required (becomes the first reply) |
category |
string | optional, max 40 |
priority |
string | low|normal|high|urgent (default normal) |
Returns the ticket with replies.
GET /tickets/{publicId}
Ticket with full replies thread (each: id, body, isStaff, authorName, createdAt).
POST /tickets/{publicId}/replies
Body: message (required). Appends a reply and reopens the ticket.
13. Real-time (Reverb / WebSockets)
Yaar Khata broadcasts live updates over Laravel Reverb (Pusher protocol).
- WebSocket URL:
wss://yaarkhata.com/app/{REVERB_APP_KEY} - Auth endpoint (private channels):
POST https://yaarkhata.com/broadcasting/authwith your bearer token. - Private channel:
group.{id}— authorization (inroutes/channels.php) only admits the group owner and its members.{id}is the group's numeric id. - Event:
App\Events\GroupDataChangedfires on expense create/update/delete and settlement create, carrying{ groupId, reason }(e.g.expense.created). Clients refetch the affected group's data on receipt.
Example with Laravel Echo:
Echo.private(`group.${groupId}`)
.listen('GroupDataChanged', (e) => refetchGroup(e.groupId));
14. Health check
GET /up — public
Laravel's built-in health endpoint. 200 when the app boots cleanly. Use it for
uptime monitors and load-balancer probes.