API reference
The Dura API is JSON over HTTPS at https://dura.muchiround.com/api/v1. It does what a member does in the app, and every write goes through the same ledger as a tap on the phone.
Authentication
Send a token on every request: Authorization: Bearer dura_.... Make tokens under Apps and agents on the account screen, or with POST /tokens (email_address, password, name). A token is shown once, acts as its owner in every household they belong to, and works until it's revoked.
| Method and path | What it does |
|---|---|
POST /tokens | Email and password in, token out. No token needed. |
GET /tokens | Your tokens (names and last use, never the values). |
DELETE /tokens/current | Revoke the token making the request. DELETE /tokens/:id revokes another. |
GET /me | You and your households. |
Conventions
- Answers:
{"data": ..., "meta": ...}on success (metaon lists),{"error": {"code", "message", "details"}}on failure. Messages are written for people. - Households: everything else is under
/households/:household_id. Anything outside your households is a404. - Private pockets: each person can keep one solo book only they can see, marked
"private": trueinGET /households. Anyone else gets a404. Don't pick it as a default household. - Money out: every amount comes as a pair,
"balance_cents": 5050, "balance": "$50.50". - Money in: amounts are currency units (
"amount": "74.99";12means twelve). Useamount_centswith a whole number to send cents. - Dates:
occurred_attakes a date (2026-10-09, recorded at noon in the household's time zone) or an ISO 8601 time. The past is fine; the future isn't. Months (2026-10) start on the household's month start day.
Safe retries
Add Idempotency-Key: any-unique-string to a POST, PATCH, PUT or DELETE. A retry with the same key returns the first answer with Idempotent-Replayed: true and records nothing new. Keys belong to your account and last 24 hours. The same key with a different request gets 422 idempotency_key_reused, and a key whose first request is still running gets 409. Requests the ledger refuses release the key, so you can fix them and retry.
Reading
| Method and path | What it returns |
|---|---|
GET /households | Your households, your role in each, and private for your private pocket. |
GET /households/:id | The table: month totals (with lent_out, what loans still owe the piggy), every envelope, IOUs, receipts waiting, streak, and loan_repayments_due. |
GET /households/:hid/envelopes | Envelopes (?include_archived=true for all). /envelopes/:id adds its last 10 entries. |
GET /households/:hid/entries | Activity, newest first. Filters: envelope_id, kind, member_id, month, since, until, include_undone. Pages: page, per_page (up to 100), meta.next_page. |
GET /households/:hid/entries/:id | One entry with its postings. |
GET /households/:hid/ious | Which envelope owes which. |
GET /households/:hid/month_summary | Budget vs spent per envelope, exchange cost and savings (?month=2026-09). |
GET /households/:hid/members | First names and roles. |
GET /households/:hid/funding_sources | Where cash comes from (Payoneer, FNB, ...). |
Recording money
| Method and path | Body |
|---|---|
POST /households/:hid/spends | envelope_id (the budget it counts against), amount, note, paid_from_envelope_id (another envelope's cash; makes an IOU), category, occurred_at, receipt (image, multipart). |
POST /households/:hid/cash_ins | cash, electronic (what left the bank; the difference is the exchange cost), funding_source_id, splits, repayments, note, occurred_at. Splits (plus any repayments) must add up to the cash: [{"envelope_id": 1, "amount": "60"}] or {"1": "60"}. repayments: [{"loan_id": 7, "amount": "183.33"}] repays piggy loans from this cash in the same step (see loans). |
POST /households/:hid/moves | from_envelope_id, to_envelope_id, amount, note. Feed the piggy bank by moving to it. |
POST /households/:hid/settlements | debtor_envelope_id, creditor_envelope_id, amount (leave it out to settle the whole IOU). |
POST /households/:hid/cash_counts | envelope_id, counted, and a reason when it doesn't match: forgot_spend, change_not_recorded, lost, found_extra, other. |
POST /households/:hid/allowances | member_id, amount, envelope_id, paid_from_envelope_id, note. Records a spend in the household (Allowance → Kuda) and puts the cash into that member's private pocket in the same step. Undoing the spend undoes both; an allowance can't be edited. Not available from a private pocket or the sample household (404). |
PATCH /households/:hid/entries/:id | A live spend's amount and/or note. Entries can't change, so this undoes the spend and records a new one: the id changes (replaced_entry_id). |
POST /households/:hid/entries/:id/reverse | Undo any entry. |
Writes answer 201 with the entry (including a plain-English summary) and the envelopes it touched, with their new balances.
Loans from the piggy
A member can borrow from the shared piggy bank (kind: "personal": the cash goes to their private pocket and they repay it from there), or the household can borrow from it (kind: "joint": the cash goes into a household envelope and any member repays it from household envelopes). Every loan starts as a request: nothing leaves the piggy until another member approves it, and the piggy's cash is checked again then. In a one-member household there's nobody to ask, so the request is paid out at once. Loans exist in shared households only; from a private pocket or the sample household these paths are 404s.
| Method and path | What it does |
|---|---|
GET /households/:hid/loans | Every loan, newest first. ?status=pending, open, behind, closed, declined or cancelled. |
GET /households/:hid/loans/:id | One loan with its schedule (each instalment: due date, amount, paid, forgiven) and history (who did what, when). |
POST /households/:hid/loans | Ask to borrow: kind, amount, months (1-60), purpose, rate (flat percent over the whole loan, default 0), start_on (first payment month; default and earliest: the first month start after today), destination_envelope_id (joint loans). Up to what the piggy holds in cash. |
POST /households/:hid/loans/:id/approve | Another member approves (optional note); the cash moves now. |
POST /households/:hid/loans/:id/decline | Another member declines (optional note). Nothing moves. |
POST /households/:hid/loans/:id/cancel | The person who asked withdraws a pending request. |
POST /households/:hid/loans/:id/repay | amount (leave it out to pay what's due now; up to what's left), from_envelope_id, note, occurred_at. Joint: any member, from a household envelope. Personal: only the borrower, from a pocket envelope (default: the pocket's first). Answers like any write, plus the loan. |
POST /households/:hid/loans/:id/skip | Push the next instalment and the rest a month on. The total doesn't change. |
POST /households/:hid/loans/:id/reschedule | Spread what's left over months from start_on. The total doesn't change. |
POST /households/:hid/loans/:id/forgive | amount, note. Anyone but the borrower (for a joint loan, anyone but who asked). Forgiving the rest closes it. |
The total is fixed when the loan is asked for: the amount plus the flat interest (rate% of it, at most 100%). Paying early pays the agreed total; skipping or rescheduling never adds to it. Every loan comes with money_lent, amount_paid_to_borrower and interest_rate_flat together, plus annual_equivalent (the APR of the plan: the monthly rate that discounts the instalments back to the amount paid out, × 12, as a whole percent) and high_annual_rate on a personal loan whose APR, before rounding, is above 5% a year (never on a joint loan): show them together. Dura never suggests a rate and charges nothing. you_can says what the token's person may do with the loan now. Undo a repayment with POST /households/:hid/entries/:id/reverse on the entry_id in its history. Everyone in the household sees a personal loan and its repayments, never the borrower's pocket.
On payday, the table's loan_repayments_due lists what's due in that book (a shared household's joint loans; in your private pocket, your personal loans), ready for repayments on a cash-in.
Savings groups (mukando)
A savings group keeps its own book, apart from any household, envelope or pocket, so its paths start with /mukando, and someone who is only in a group can use them. You see a group only through your own membership; anything else is a 404. Members are first names and a member_id, never emails. Amounts are in the group's currency and months are counted from 1. Every answer carries meta.records_only: Dura keeps the group's records and never holds or moves its money.
The keeper records; each member an entry involves confirms it. An entry counts only once everyone involved has confirmed it, and waiting or disputed money is always shown apart. Nothing is edited: the keeper reverses an entry and records it again. Refusals are 422 mukando_error with a message for people, and a keeper's action by anyone else is 403 keeper_only.
| Method and path | What it does |
|---|---|
GET /mukando/groups | Your groups: the running cycle's rules (cycle.rules in plain words) and the pool (total, waiting, disputed). |
GET /mukando/groups/:gid | The group home: members, this month (who gets the pot, who has paid), your position, where the money is, loans, what's waiting for you. |
GET /mukando/groups/:gid/position | What you've paid in, what you owe, your payout month, your loans and your share-out if the cycle ended today, with its breakdown. |
GET /mukando/groups/:gid/rotation | Every month's recipient, pot and each member's state (paid, waiting, disputed, part, missing, not_due), plus swaps waiting. |
GET /mukando/groups/:gid/entries | The book, newest first; status, kind, cycle, page, per_page. GET /mukando/groups/:gid/entries/:id adds the note and each confirmation. |
POST /mukando/groups/:gid/entries | The keeper records one kind: contribution (member_id, month, location_id, round_amount, savings_amount), payout (month, location_id, amount), loan (member_id, amount, location_id; dry_run: true checks it and saves nothing), repayment (loan_id, amount, location_id), handover (amount, from_location_id, to_location_id), share_out or write_off (member_id, note). Plus occurred_on and note. |
POST /mukando/groups/:gid/entries/:id/reverse | The keeper reverses an entry (optional note); record the right one again. |
GET /mukando/groups/:gid/locations | Each place's confirmed balance with waiting and disputed changes apart; they add up to meta.pool_total. The keeper adds one with POST /mukando/groups/:gid/locations (holder_member_id, kind cash or account, label) and puts an empty one away with POST /mukando/groups/:gid/locations/:id/archive. |
GET /mukando/groups/:gid/loans | Pool loans (?state=pending, open, repaid, void); GET /mukando/groups/:gid/loans/:id adds its entries. |
GET /mukando/confirmations | What's waiting for you across your groups and, in groups you keep, what was disputed. |
POST /mukando/confirmations/:id/confirm | People only: your own word that the money moved. POST /mukando/confirmations/:id/dispute takes a note saying what's wrong. |
POST /mukando/groups/:gid/swaps | Ask another member (with_member_id) to swap payout months. She answers with POST /mukando/groups/:gid/swaps/:id/accept or /decline; you can /cancel. |
POST /mukando/groups/:gid/keeper | The keeper offers the book to member_id. She answers with POST /mukando/groups/:gid/keeper/accept or /decline. |
GET /mukando/groups/:gid/share_out | Every member's share-out line and how it's made up. From the cycle's last day the keeper finalises with POST /mukando/groups/:gid/share_out/finalise, and each member confirms what she received. |
People only. Confirming, disputing, answering a swap or a keeper offer, recording a write-off and finalising a share-out are a person's own decisions. An agent never makes them on its own. Every pool loan carries money_lent, amount_paid_to_borrower and interest_rate_flat together with annual_equivalent, and above 5% a year rate_warning: show it word for word. Dura never suggests a charge; the group sets it when the cycle starts. Starting a group, inviting, joining, starting a cycle, leaving and exporting your records happen in the app.
Receipt scans
| Method and path | What it does |
|---|---|
POST /households/:hid/receipt_scans | Upload image (photo or PDF, multipart). Starts in reading. |
GET /households/:hid/receipt_scans/:id | Poll until ready: lines with a suggested envelope each. failed means add the lines yourself. |
POST /households/:hid/receipt_scans/:id/confirm | Records one spend per envelope. No body accepts the suggestions; or send lines: [{name, amount, envelope_id}] and paid_from_envelope_id. |
DELETE /households/:hid/receipt_scans/:id | Discard. GET /households/:hid/receipt_scans lists the ones waiting. |
Envelopes and funding sources
POST /households/:hid/envelopes takes name, emoji, color and monthly_budget; PATCH changes them (the piggy bank also takes goal and goal_name); DELETE archives an empty envelope and keeps its history. Funding sources have POST (name), PATCH and DELETE (archive) too.
The envelope order is the same for everyone in the household. PUT /households/:hid/envelopes/order takes envelope_ids, every active envelope from top to bottom (leave out the piggy bank; it stays at the end), and answers with the envelopes in their new order. If someone added or archived an envelope since you read the list, you get 409 arrangement_stale with the current order in details.envelope_ids, and nothing changes. How the table looks (scattered, neat, list or groups) is each person's own setting in the app, not part of the API.
Groups are named sections of the table (Bills, Kids, Fun), the same for everyone in the household. GET /households/:hid/envelope_groups lists them in order with their envelope_ids; POST takes a name (up to 30 characters, unique in the household), PATCH /households/:hid/envelope_groups/:id renames, and DELETE removes the group while its envelopes stay, ungrouped. Put an envelope in a group with group_id on PATCH /households/:hid/envelopes/:id (null takes it out; the piggy bank is never in a group). PUT /households/:hid/envelope_groups/order saves the whole arrangement at once: groups: [{id, envelope_ids}] in order plus ungrouped_envelope_ids, every group and active envelope once. It answers 409 arrangement_stale if anything changed since you read it, like the envelope order.
Errors
| Status | Code | When |
|---|---|---|
| 400 | parameter_missing, bad_request | A field is missing, or an amount or date can't be read. |
| 401 | unauthorized, invalid_credentials | No token, a revoked token, or a wrong password. |
| 403 | keeper_only | Only a savings group's keeper records, reverses, adds places and finalises. |
| 404 | not_found, route_not_found | Not in your household or savings group, or no such endpoint. |
| 409 | idempotency_key_in_progress, arrangement_stale | The first request with that key is still running, or the envelopes or groups changed since you read their order. |
| 422 | ledger_error | The ledger refused it, for example "Splits add up to $90 but you have $100 in cash." Nothing was written. |
| 422 | mukando_error | A savings group's rules refused it, for example "The pool can lend $60 (savings and repayments only)." Nothing was written. |
| 422 | invalid, not_editable, idempotency_key_reused, invalid_envelope_ids | A field failed validation (see details), the entry can't be edited, a key was reused, or an envelope order names an envelope that isn't one of this household's. |
| 429 | rate_limited, daily_limit | More than 600 requests in 5 minutes on one token, or too many receipts today. |