mytmn-hub

REST API that fetches, normalizes, and caches MyTaman billing data as structured JSON (dashboard, paid invoices, billing invoices, profiles). Integrate using the endpoints below; use the machine-readable OpenAPI spec or Postman collection for your stack.

REST JSON OpenAPI 3.0 Bearer auth

Quick start

1. Authenticate with your MyTaman credentials. You receive a short-lived access token and a long-lived refreshToken (store both server-side in your app):

# Login
curl -X POST https://mytmn.sofehaus.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"s3cret"}'

# Response: {"token":"…","refreshToken":"…"}

2. Use the access token for API calls. If you get 401 unauthorized (expired access token), mint a new one without re-logging in:

# Refresh access token
curl -X POST https://mytmn.sofehaus.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"<paste refreshToken>"}'

# Response: {"token":"…"}

3. Use the current access token for all data requests:

# Get your taman profile
curl https://mytmn.sofehaus.com/api/profile \
  -H "Authorization: Bearer <token>"

# List all invoice profiles
curl https://mytmn.sofehaus.com/api/billing/invoices \
  -H "Authorization: Bearer <token>"

# Paid Invoices table (optional ?page=, paid_at_*, invoice_due_*, online_offline_search)
curl "https://mytmn.sofehaus.com/api/billing/paid-invoices?page=1&online_offline_search=Only+Online" \
  -H "Authorization: Bearer <token>"

# Billing Invoices table (optional ?page=, search params match portal Ransack keys)
curl "https://mytmn.sofehaus.com/api/billing/issued-invoices?page=1&status_eq=Paid" \
  -H "Authorization: Bearer <token>"

# Get stats for a specific invoice
curl https://mytmn.sofehaus.com/api/billing/invoices/9978/stats \
  -H "Authorization: Bearer <token>"

4. Logout when done (invalidates access + refresh on the server and clears TTL cache):

curl -X POST https://mytmn.sofehaus.com/api/auth/logout \
  -H "Authorization: Bearer <access token>"
Sessions: Persist server-side across process restarts when the host supports durable storage. Cache: In-memory per stable session id — profile (30 min), invoice profiles (10 min), issued-invoices pages (10 min per page), stats (5 min). Keys stay valid across access-token refresh. Logout evicts all cached data for that session.

API reference

GET /health

Service health check. No authentication required.

// 200 OK
{ "ok": true, "service": "mytmn-hub" }

POST /api/auth/login

Authenticate with MyTaman. Returns short-lived token (use as Bearer) and long-lived refreshToken. Upstream _bendefence_session is kept only on the server. Access / refresh TTLs follow env ACCESS_TOKEN_TTL_MS and REFRESH_TOKEN_TTL_MS.

Body FieldTypeRequiredDescription
emailstringYesMyTaman email
passwordstringYesMyTaman password
// 201 Created
{
  "token": "c0a80164-1234-5678-9abc-def012345678",
  "refreshToken": "d1b91275-2345-6789-abcd-ef1234567890"
}
StatusMeaning
201Session created; tokens returned
400invalid_json or invalid_body (e.g. missing email/password)
401login_failed — MyTaman rejected credentials
500internal_error — e.g. could not persist session

POST /api/auth/refresh

Exchange refreshToken for a new access token. Does not rotate the refresh token. No Authorization header.

Body FieldTypeRequiredDescription
refreshTokenstring (UUID)YesValue from login response
// 200 OK
{ "token": "e2ca2386-3456-789a-bcde-f01234567890" }
StatusMeaning
200New Bearer token; TTL cache for that session id unchanged
400invalid_json or invalid_body / missing refreshToken
401unauthorizeddetail: invalid_or_expired_refresh_token

POST /api/auth/logout

Optional Authorization: Bearer <access token>. If the token resolves to a session, stored session data (access, refresh, cookie mapping) is removed and in-memory TTL cache for that session id is cleared. **Always 204 No Content** — including when the header is omitted, or the token is unknown/expired (idempotent). After access expiry, call refresh first, then logout with the new token to revoke the refresh token.

StatusMeaning
204Empty body; session deleted when a valid access token was sent

GET /api/profile Bearer

Taman name and address parsed from MyTaman navigation bar. Cached 30 min.

TamanInfo

namestringTaman name
addressstringPrimary house address
// 200 OK
{ "name": "Taman Bukit Indah", "address": "No 1, Jalan Bukit Indah 1/1" }

GET /api/billing/invoices Bearer

All invoice profiles from the dashboard dropdown. Cached 10 min.

InvoiceProfileOption[]

idintegerInvoice profile ID
namestringDisplay name
selectedbooleanCurrently active profile
// 200 OK
[
  { "id": 9978, "name": "Monthly Maintenance", "selected": true },
  { "id": 10234, "name": "Sinking Fund", "selected": false }
]

GET /api/billing/paid-invoices Bearer

Parsed rows from MyTaman Paid Invoices (/ra_dashboard/paid_invoices). Use page (1-based) and the same filter fields as the RA search form. Cached 10 min per page + filter set.

Query paramUpstream
pagePagination
paid_at_gteq, paid_at_lteqPaid date range
invoice_due_gteq, invoice_due_lteqDue date range
online_offline_searchShow All, Only Online, Only Offline

PaidInvoicesPage

invoicesPaidInvoice[]Table rows (grouping/subtotal rows omitted)
pageinteger
lastPageinteger?
hasNextboolean
// 200 OK (abbrev.)
{
  "invoices": [
    {
      "id": 631729,
      "invoiceNumber": "318900631729",
      "userName": "JOHAN BIN AHMAD",
      "houseAddress": "741, Jalan Nada Alam 6/2",
      "amount": 70,
      "pdfPath": "/ra_dashboard/invoices/631729.pdf",
      "receiptPath": "/ra_dashboard/invoices/631729/receipt"
    }
  ],
  "page": 1,
  "lastPage": 108,
  "hasNext": true
}

GET /api/billing/issued-invoices Bearer

Parsed rows from MyTaman Billing Invoices (/ra_dashboard/invoices). Use page (1-based) plus any of the search query params below (same semantics as the RA search form; sort maps to upstream q[s]). Cached 10 min per page + filter set.

Query paramUpstream
pagePagination
invoice_title_cont, invoice_number_contq[invoice_*]
status_eq, status_not_eqStatus filters
house_address_cont_all, house_address_eqAddress
user_name_cont_allResident name
invoice_profile_id_eqInvoice profile id
created_at_gteq, created_at_lteqIssued date range
paid_at_gteq, paid_at_lteqPaid date range
invoice_due_gteq, invoice_due_lteqDue date range
unpaid_invoice_pass_due_searchUnpaid past due (portal value)
sorte.g. invoice_number asc

IssuedInvoicesPage

invoicesIssuedInvoice[]Table rows
pageintegerCurrent page
lastPageinteger?From pagination “Last” link
hasNextbooleanrel=next present
// 200 OK (abbrev.)
{
  "invoices": [
    {
      "id": 654037,
      "invoiceNumber": "318900654037",
      "title": "Additional Access Card #4",
      "status": "Paid",
      "amount": 15,
      "editPath": "/ra_dashboard/invoices/654037/edit",
      "pdfPath": "/ra_dashboard/invoices/654037.pdf",
      "receiptPath": "/ra_dashboard/invoices/654037/receipt"
    }
  ],
  "page": 1,
  "lastPage": 618,
  "hasNext": true
}

GET /api/billing/invoices/:id/stats Bearer

Billing statistics for one invoice profile. Cached 5 min. The :id is an invoice_profile_id from the list above.

CurrentInvoiceProfile

idintegerInvoice profile ID
titlestringInvoice title
frequencystringe.g. "Monthly", "One Off"
startDatestringBilling start date
totalIssuedBillTotal{count, amount}
totalCollectedBillTotal{count, amount}
totalOutstandingBillTotal{count, amount}
paidHousesobject{paid: int, unpaid: int}
// 200 OK
{
  "id": 9978,
  "title": "Monthly Maintenance",
  "frequency": "Monthly",
  "startDate": "2024-01-01",
  "totalIssued": { "count": 271, "amount": 21680.00 },
  "totalCollected": { "count": 258, "amount": 19820.00 },
  "totalOutstanding": { "count": 13, "amount": 1860.00 },
  "paidHouses": { "paid": 279, "unpaid": 18 }
}

GET /api/billing/dashboard Bearer

Full composite dashboard: taman info + all invoice profiles + current profile stats. Optional ?profile_id= to load a specific profile. Cached 5 min.

Query ParamTypeRequiredDescription
profile_idintegerNoLoad specific invoice profile
// 200 OK
{
  "taman": { "name": "...", "address": "..." },
  "profiles": [ ... ],
  "current": { ... }
}

Error handling

All errors return JSON with an error code and optional detail.

ErrorResponse

errorstringMachine-readable error code
detailstring?Optional human-readable context
StatusError CodeWhen
400invalid_jsonRequest body is not valid JSON
400invalid_bodyMissing required fields or invalid path param
401unauthorizedMissing / invalid / expired Bearer token
401login_failedMyTaman rejected credentials or session expired
404not_foundUnknown route
502upstream_errorMyTaman returned non-200
500internal_errorUnexpected server error

Caching strategy

Responses are cached in-memory per stable session id (not the Bearer string), so refreshing the access token does not flush the TTL cache.

ResourceCache KeyTTL
Taman profile{sessionId}:profile30 min
Invoice profiles{sessionId}:invoices10 min
Paid invoices{sessionId}:paid-invoices[:filters]:{page}10 min
Issued invoices{sessionId}:issued-invoices[:filters]:{page}10 min
Invoice stats{sessionId}:invoice:{id}5 min
Full dashboard{sessionId}:dashboard:{id|default}5 min
Invalidation: Logging out (POST /api/auth/logout) evicts all cached entries for that session. Cache entries also self-expire via lazy TTL check on read.

Architecture

How a protected read flows through the hub: Bearer access token resolves (server-side lookup + small in-memory cache) to a stable session id and MyTaman cookie; repositories consult the TTL cache before touching upstream.

New MyTaman screens follow the same pattern: dedicated fetch module + parser + repository slice, sharing the global CacheStore and session cookie from login.

Release notes

API Changelog below describe changes that may affect your integration (HTTP surface, auth, JSON fields, caching visible to clients). Fetch GET /release-notes for this same content over the wire.

Release notes are also available in the repo under CHANGELOG.md

Downloads & tools

Postman tip: Import the collection, set baseUrl, run Auth > Login — the test script saves token and refreshToken. Use Auth > Refresh when the access token expires.