YYaar Khata API

Yaar Khata API Reference

REST API for the Yaar Khata expense-splitting platform. It powers the Flutter mobile app and any third-party client.


Table of contents

  1. Authentication
  2. Conventions
  3. Errors
  4. Auth & profile endpoints
  5. Content endpoints (public)
  6. Groups
  7. Expenses
  8. Settlements
  9. Group data (activities, balances, settle-up)
  10. Categories
  11. Coupons
  12. Support tickets
  13. Real-time (Reverb / WebSockets)
  14. Health check

1. Authentication

Yaar Khata uses Sanctum personal access tokens.

  1. Call POST /register or POST /login. The response contains a token.
  2. 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).

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.