Files
jacobh2 50a0d6792a
Build Kvitt images / build (push) Failing after 1m20s
Server: web client under /app/ (D-132), analytics E-34 trends and E-35 forecast (D-133)
Part A (D-132): KVITT_WEB_DIR (default /app) served under /app/ by a small handler: /app redirects to
/app/, app routes fall back to index.html, a missing assets/* file is 404, assets cached immutable,
index.html no-store, the app's own CSP plus nosniff, DENY and no-referrer. Unsafe paths never reach the
file system. /app paths count against the unauthenticated per-IP limit. Without the directory every /app
path stays 404 not_found. server/Dockerfile gains a Node 24 LTS stage (pinned by digest) building web/,
passed through the Rust stage so npm and rustc never run at once on the Pi 3; the build context is the
repository root with an allowlist .dockerignore (server/.dockerignore removed); workflow, build-images.sh
and infra/dev/build.sh follow.

Part B (D-133): GET /analytics/trends (E-34) and GET /analytics/forecast (E-35), authenticated, read-only,
SQL month buckets; rules (half-up average, trailing 3-month moving average, percent against the rounded
average, template occurrence per month) in kvitt-domain::analytics. OpenAPI, examples, contracts-check
and tests updated (35 operations).

Cargo.lock: rustls-webpki 0.103.16 and webpki-root-certs 1.0.10 (RUSTSEC-2026-0382, -0383) so that
cargo audit passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-21 20:54:21 +02:00

2756 lines
94 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kvitt HTTP API, written by hand from SPEC v0.1 §7 and §14.3 (milestone M1). Schema version 1, sync protocol 1.
# SPEC §14.3 is binding; a change to this file follows the contract-change rule of SPEC §14.8.
# Value limits (lengths, ranges) are not encoded as JSON Schema constraints: a value outside SPEC §9.6 is
# 422 validation_failed, not 400 invalid_request (SPEC §14.3.1), so the limits stay in SPEC §9.6.
openapi: 3.1.0
info:
title: Kvitt
version: '1'
description: |
Shared-expense server for a two-person household (SPEC.md). Money is integer öre, timestamps are
RFC 3339 UTC with millisecond precision (`2026-10-17T06:00:00.000Z`), ids are lowercase UUIDv7 strings.
Common rules (SPEC §14.3.1):
* Every JSON body is `application/json; charset=utf-8`.
* Unknown JSON fields in requests are ignored. Missing required fields or wrong types: 400 `invalid_request`.
Values outside SPEC §9.6: 422 `validation_failed` with `field`.
* Optional fields in responses are present with `null`, never omitted.
* `PATCH` bodies: every field optional; absent = unchanged; at least one field required (`invalid_request`).
* Path ids that are not a UUID: 404 `not_found`.
* Authenticated endpoints without `X-Kvitt-Protocol: 1`: 426 `protocol_unsupported`.
* Every response carries `X-Kvitt-Server-Version`.
* Besides the codes listed per operation, authenticated operations can return `unauthorized`, `device_revoked`,
`protocol_unsupported`, `rate_limited`, `internal`, `unavailable`; unauthenticated ones `rate_limited`,
`internal`, `unavailable`. `not_implemented` (501) exists only during milestones M2a to M4.
servers:
- url: https://{host}
variables:
host:
default: kvitt.example.invalid
description: the owner-chosen Kvitt host name, SPEC §10.5
security:
- device: []
paths:
/health:
get:
operationId: getHealth
summary: E-1 Liveness and DB check
security: []
parameters:
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: Server and database are up.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Health'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
description: |
The database check (`SELECT 1`) failed: the `Health` shape with `status` `degraded` and `db_ok` false.
An `Error` with code `unavailable` when the server is shutting down.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
# TODO(spec): §14.3.4 E-1 gives 503 the Health shape; §14.3.2 gives every endpoint 503 `unavailable` (Error shape).
oneOf:
- $ref: '#/components/schemas/Health'
- $ref: '#/components/schemas/Error'
/setup/status:
get:
operationId: getSetupStatus
summary: E-2 Whether the household has been bootstrapped
security: []
parameters:
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: '`initialized` is true once the admin_setup invite has been accepted (AC-1.5).'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/SetupStatus'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/invites/accept:
post:
operationId: acceptInvite
summary: E-3 Pair a device with an invite code (all three invite kinds)
security: []
parameters:
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/AcceptInviteRequest'
responses:
'200':
description: Paired. The token is shown only here.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/AcceptInviteResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request` (also a missing `display_name` for admin_setup and partner), `invite_invalid`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`household_full`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'429':
$ref: '#/components/responses/RateLimited'
description: '`rate_limited`, also after more than 10 failed accepts from one IP within 60 minutes (AC-2.12).'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/i/{code}:
get:
operationId: getInviteLanding
summary: E-4 Invite landing page (HTML)
description: |
HTML per AC-2.5 for any `code` of 1 to 32 characters; validity is not checked. Swedish when
`Accept-Language` prefers `sv`, English otherwise.
security: []
parameters:
- name: code
in: path
required: true
schema:
type: string
- name: from
in: query
required: false
description: The inviting admin's display name (E-12 link), passed on into the `kvitt://invite` link; an invalid value is ignored.
schema:
type: string
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: Landing page.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
text/html; charset=utf-8:
schema:
type: string
'404':
description: '`code` not 1 to 32 characters: an HTML 404 page.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
text/html; charset=utf-8:
schema:
type: string
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/apk/latest:
get:
operationId: getApkLatest
summary: E-5 Latest APK metadata
security: []
parameters:
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The content of `latest.json` (SPEC §10.6), read on every request.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/ApkLatest'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found` (nothing published).'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/apk/files/{file_name}:
get:
operationId: getApkFile
summary: E-6 APK download
security: []
parameters:
- name: file_name
in: path
required: true
description: Served only when it matches `^kvitt-[0-9]+\.apk$` and exists (SPEC §10.6).
schema:
type: string
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The APK bytes.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
Content-Length:
required: true
schema:
type: integer
content:
application/vnd.android.package-archive:
schema:
type: string
contentMediaType: application/vnd.android.package-archive
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found` (unknown APK file).'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/me:
get:
operationId: getMe
summary: E-7 Current user, device and household users
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The caller's user and device.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/MeResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
patch:
operationId: patchMe
summary: E-8 Change own display name
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/PatchMeRequest'
responses:
'200':
description: The updated user.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/me/picture:
put:
operationId: putMePicture
summary: E-9 Upload own picture
description: JPEG, PNG or WebP up to 5 MiB and 8000 × 8000 px; processed per AC-4.4.
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
image/jpeg:
schema:
type: string
contentMediaType: image/jpeg
image/png:
schema:
type: string
contentMediaType: image/png
image/webp:
schema:
type: string
contentMediaType: image/webp
responses:
'200':
description: The user with the new `picture_version`.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/User'
'401':
$ref: '#/components/responses/Unauthorized'
'413':
$ref: '#/components/responses/PayloadTooLarge'
description: '`payload_too_large` (over 5 MiB).'
'415':
$ref: '#/components/responses/UnsupportedMediaType'
description: '`unsupported_media_type`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`image_invalid`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
delete:
operationId: deleteMePicture
summary: E-10 Remove own picture
description: Idempotent.
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The user with `picture_version` null.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/User'
'401':
$ref: '#/components/responses/Unauthorized'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/users/{user_id}/picture:
get:
operationId: getUserPicture
summary: E-11 Download a user's picture
parameters:
- $ref: '#/components/parameters/UserId'
- name: size
in: query
required: false
description: Other values are `invalid_request`.
schema:
type: integer
enum: [128, 512]
default: 512
- name: v
in: query
required: false
description: The `picture_version`; a cache key only, ignored by the server.
schema:
type: integer
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The square JPEG.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
ETag:
description: '`"<picture_version>"`'
required: true
schema:
type: string
Cache-Control:
description: '`private, max-age=31536000, immutable`'
required: true
schema:
type: string
content:
image/jpeg:
schema:
type: string
contentMediaType: image/jpeg
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request` (bad `size`).'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found` (unknown user or no picture).'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/invites:
post:
operationId: createInvite
summary: E-12 Create a partner or device invite (admin)
description: Creating a `partner` invite cancels any pending one (AC-2.1).
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CreateInviteRequest'
responses:
'201':
description: The invite with `code` (12 characters, no hyphens) and `link` (`<public_url>/i/<code>?from=<admin display name, percent-encoded>`).
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Invite'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
description: '`forbidden` (caller is not the admin).'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found` (target user).'
'409':
$ref: '#/components/responses/Conflict'
description: '`household_full`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
get:
operationId: listInvites
summary: E-13 List pending invites (admin)
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: Pending invites only (not accepted, not cancelled, not expired), `code` and `link` null.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/InviteList'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
description: '`forbidden`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/invites/{invite_id}:
delete:
operationId: cancelInvite
summary: E-14 Cancel an invite (admin)
description: Idempotent for an already cancelled invite.
parameters:
- $ref: '#/components/parameters/InviteId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'204':
description: Cancelled.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
description: '`forbidden`.'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/devices:
get:
operationId: listDevices
summary: E-15 List devices (admin all, member own)
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: Ordered by `created_at`, revoked devices included (with `revoked_at`).
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/DeviceList'
'401':
$ref: '#/components/responses/Unauthorized'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/devices/{device_id}:
delete:
operationId: revokeDevice
summary: E-16 Revoke a device
description: Revoking an already revoked device is 204. Admin any device except the current one; member own devices except the current one (SPEC §8.4).
parameters:
- $ref: '#/components/parameters/DeviceId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'204':
description: Revoked.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
description: '`forbidden`.'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`cannot_revoke_current_device`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/devices/current/push-token:
put:
operationId: putPushToken
summary: E-17 Set or clear this device's FCM token
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/PushTokenRequest'
responses:
'204':
description: Stored (or cleared).
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/accounts:
post:
operationId: createAccount
summary: E-18 Create account
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CreateAccountRequest'
responses:
'201':
description: The new account.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Account'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
description: '`name_taken`, `household_incomplete`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/accounts/{account_id}:
patch:
operationId: patchAccount
summary: E-19 Rename, change default ratio
parameters:
- $ref: '#/components/parameters/AccountId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/PatchAccountRequest'
responses:
'200':
description: The updated account.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Account'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`name_taken`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
delete:
operationId: deleteAccount
summary: E-22 Soft delete when empty
description: Refused while a non-deleted expense, an allocation of a non-deleted settlement or a non-deleted recurring template references the account (AC-6.5).
parameters:
- $ref: '#/components/parameters/AccountId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The tombstone.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Account'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`account_not_empty`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/accounts/{account_id}/archive:
post:
operationId: archiveAccount
summary: E-20 Archive account
description: No body. Idempotent.
parameters:
- $ref: '#/components/parameters/AccountId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The account.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Account'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/accounts/{account_id}/unarchive:
post:
operationId: unarchiveAccount
summary: E-21 Unarchive account
description: No body. Idempotent.
parameters:
- $ref: '#/components/parameters/AccountId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The account.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Account'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/categories:
post:
operationId: createCategory
summary: E-23 Create category
description: New categories get `sort_order` = current maximum + 10 (AC-7.3). `icon` and `color` are tokens of SPEC §9.7.
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CreateCategoryRequest'
responses:
'201':
description: The new category.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Category'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/categories/{category_id}:
patch:
operationId: patchCategory
summary: E-24 Rename, icon, colour, order, reset name
description: |
`name: null` resets a default category's name. Reordering several categories is one PATCH per category whose
`sort_order` changes.
parameters:
- $ref: '#/components/parameters/CategoryId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/PatchCategoryRequest'
responses:
'200':
description: The updated category.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Category'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`category_is_default` (`name: null` on a non-default category).'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
delete:
operationId: deleteCategory
summary: E-27 Soft delete when unused and not default
parameters:
- $ref: '#/components/parameters/CategoryId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The tombstone.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Category'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`category_in_use`, `category_is_default`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/categories/{category_id}/archive:
post:
operationId: archiveCategory
summary: E-25 Archive category
description: No body.
parameters:
- $ref: '#/components/parameters/CategoryId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The category.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Category'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/categories/{category_id}/unarchive:
post:
operationId: unarchiveCategory
summary: E-26 Unarchive category
description: No body.
parameters:
- $ref: '#/components/parameters/CategoryId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The category.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Category'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/recurring-templates:
post:
operationId: createRecurringTemplate
summary: E-28 Create recurring template
description: If a period is already due (SPEC §9.5), the server generates it before responding.
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CreateRecurringTemplateRequest'
responses:
'201':
description: The new template.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/RecurringTemplate'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
description: '`household_incomplete`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed` (also for an unknown or deleted `account_id` or `category_id`, with `field`).'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/recurring-templates/{template_id}:
patch:
operationId: patchRecurringTemplate
summary: E-29 Edit, pause, resume
description: Any subset of the E-28 fields; `split_mode` and `splits` must be sent together.
parameters:
- $ref: '#/components/parameters/TemplateId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/PatchRecurringTemplateRequest'
responses:
'200':
description: The updated template.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/RecurringTemplate'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
delete:
operationId: deleteRecurringTemplate
summary: E-30 Soft delete recurring template
parameters:
- $ref: '#/components/parameters/TemplateId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The tombstone.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/RecurringTemplate'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/sync:
get:
operationId: syncPull
summary: E-31 Pull changes since a cursor
description: SPEC §14.4.2.
parameters:
- name: since
in: query
# TODO(spec): §14.4.2 does not say whether `since` may be omitted; required here (the phone's cursor starts at 0).
required: true
description: The highest `server_version` the phone has applied (0 initially).
schema:
type: integer
format: int64
- name: limit
in: query
required: false
description: Rows per page, 1 to 1000 (`invalid_request` otherwise).
schema:
type: integer
default: 500
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: One page. Every array is present (possibly empty) and sorted by `server_version`.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/SyncPullResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
description: '`cursor_ahead`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
post:
operationId: syncPush
summary: E-32 Push local expense and settlement changes
description: SPEC §14.4.3 and §14.4.4. 1 to 100 mutations, body at most 1 MiB. One result per mutation, in request order.
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/SyncPushRequest'
responses:
'200':
description: Per-mutation results.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/SyncPushResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
description: '`household_incomplete` (whole request, before any mutation).'
'413':
$ref: '#/components/responses/PayloadTooLarge'
description: '`payload_too_large`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`batch_too_large` (more than 100 mutations).'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/settlements/proposal:
post:
operationId: proposeSettlement
summary: E-33 Server-side allocation proposal
description: Computed with `kvitt-domain::propose_allocation` (SPEC §9.4) over the server's non-deleted rows.
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
requestBody:
required: true
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/ProposalRequest'
responses:
'200':
description: The proposal.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/ProposalResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
description: '`no_accounts`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/analytics/trends:
get:
operationId: getAnalyticsTrends
summary: E-34 Spending trends and category averages
description: |
D-133, SPEC §14.5.7. The last `months` whole calendar months before the current one (Europe/Stockholm), for
the household and per category: each month's spend, the trailing 3-month moving average (the first months
average what exists, D-128), the total, the average per month (half-up to öre, empty months count), the last
whole month against the average in whole percent (half-up; null when the average is 0), and the current month
so far. Non-deleted expenses only; settlements never count; archived and deleted categories count.
parameters:
- name: months
in: query
required: false
description: Whole months, 1 to 24 (`invalid_request` otherwise).
schema:
type: integer
default: 6
- name: measure
in: query
required: false
description: '`household` (full amounts, whoever paid) or `my_share` (the calling user''s shares), AC-14.4; other values are `invalid_request`.'
schema:
type: string
enum: [household, my_share]
default: household
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The trends.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/TrendsResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request` (bad `months` or `measure`, with `field`).'
'401':
$ref: '#/components/responses/Unauthorized'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/analytics/forecast:
get:
operationId: getAnalyticsForecast
summary: E-35 Spending forecast per account
description: |
D-133, SPEC §14.5.7. For every non-deleted account (archived included) and each of the two calendar months
after the current one (Europe/Stockholm): `recurring_ore` = the amounts of the account's non-paused,
non-deleted recurring templates due in that month (one per template and month, within `start_period` and
`end_period`); `baseline_ore` = the account's spend over the last 3 whole months (the window of D-128's moving
average), expenses generated from a recurring template excluded, divided by 3 (half-up to öre; months without
spend count, so a new account has a lower baseline); `forecast_ore = recurring_ore + baseline_ore`. `current_month_ore` is the current month so
far, generated expenses included. `my_share` uses the shares a generation would write today.
parameters:
- name: measure
in: query
required: false
description: '`household` (full amounts, whoever paid) or `my_share` (the calling user''s shares), AC-14.4; other values are `invalid_request`.'
schema:
type: string
enum: [household, my_share]
default: household
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The forecast.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/ForecastResponse'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request` (bad `months` or `measure`, with `field`).'
'401':
$ref: '#/components/responses/Unauthorized'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
components:
securitySchemes:
device:
type: http
scheme: bearer
description: |
`Authorization: Bearer kvt_<43 base64url characters>` (SPEC §8.2). Missing or unknown token: 401
`unauthorized`; revoked device: 401 `device_revoked`. Admin-only operations (E-12, E-13, E-14) return 403
`forbidden` for a member (SPEC §8.4).
parameters:
Protocol:
name: X-Kvitt-Protocol
in: header
required: true
description: Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`.
schema:
type: string
enum: ['1']
ProtocolOptional:
name: X-Kvitt-Protocol
in: header
required: false
description: Sent by the app on every call; not enforced on unauthenticated endpoints.
schema:
type: string
enum: ['1']
AppVersion:
name: X-Kvitt-App-Version
in: header
required: false
description: The app's `versionCode`, sent by the app on every call.
schema:
type: integer
AppVersionName:
name: X-Kvitt-App-Version-Name
in: header
required: false
description: The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored).
schema:
type: string
AcceptLanguage:
name: Accept-Language
in: header
required: false
description: The device locale.
schema:
type: string
UserId:
name: user_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
InviteId:
name: invite_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
DeviceId:
name: device_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
AccountId:
name: account_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
CategoryId:
name: category_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
TemplateId:
name: template_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
headers:
ServerVersion:
description: The server crate version.
required: true
schema:
type: string
RetryAfter:
description: Seconds until the client may retry.
required: true
schema:
type: integer
responses:
BadRequest:
description: '`invalid_request` or `invite_invalid`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: '`unauthorized` or `device_revoked`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: '`forbidden`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: '`not_found`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A 409 error code of SPEC §14.3.2.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
PayloadTooLarge:
description: '`payload_too_large`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
UnsupportedMediaType:
description: '`unsupported_media_type`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
Unprocessable:
description: A 422 error code of SPEC §14.3.2.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
ProtocolUnsupported:
description: '`protocol_unsupported`.'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: '`rate_limited` (SPEC §8.6).'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
Internal:
description: '`internal` (logged with a request id, returned in `message`).'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
NotImplemented:
description: '`not_implemented` (route registered but not yet implemented; only during milestones M2a to M4).'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
Unavailable:
description: '`unavailable` (database busy beyond `busy_timeout`, or server shutting down).'
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
schemas:
# ---- primitives (SPEC conventions, §14.3.3) ----
Uuid:
type: string
format: uuid
description: Lowercase, hyphenated, 36 characters (UUIDv7).
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
Timestamp:
type: string
format: date-time
description: RFC 3339 UTC, millisecond precision, literal Z.
pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
Date:
type: string
format: date
pattern: '^\d{4}-\d{2}-\d{2}$'
Period:
type: string
description: A month, YYYY-MM.
pattern: '^\d{4}-(0[1-9]|1[0-2])$'
Ore:
type: integer
format: int64
description: Money in öre (1/100 SEK).
ServerVersionNumber:
type: integer
format: int64
description: From the one global, strictly increasing sequence (SPEC §5.2).
# ---- enums ----
Role:
type: string
enum: [admin, member]
InviteKind:
type: string
enum: [admin_setup, partner, device]
DefaultCategoryKey:
type: string
enum: [groceries, restaurants, home, utilities, transport, travel, entertainment, health, shopping, other, housing, insurance, children, fees]
SplitMode:
type: string
enum: [default, ratio, exact, full]
AnalyticsMeasure:
type: string
description: AC-14.4. `household` sums full expense amounts, whoever paid; `my_share` the calling user's shares.
enum: [household, my_share]
EntityType:
type: string
enum: [expense, settlement]
AuditAction:
type: string
enum: [create, update, delete]
AuditSource:
type: string
enum: [app, recurring, import]
ErrorCode:
type: string
description: Exhaustive (SPEC §14.3.2). The app maps `code` (and `field`) to localised strings.
enum:
- invalid_request
- invite_invalid
- unauthorized
- device_revoked
- forbidden
- not_found
- household_full
- household_incomplete
- name_taken
- account_not_empty
- category_in_use
- category_is_default
- cannot_revoke_current_device
- no_accounts
- cursor_ahead
- payload_too_large
- unsupported_media_type
- validation_failed
- image_invalid
- batch_too_large
- protocol_unsupported
- rate_limited
- internal
- not_implemented
- unavailable
RejectionReason:
type: string
description: Exhaustive (SPEC §14.4.6).
enum:
- stale
- deleted
- validation_failed
- reference_not_found
- clock_skew
# ---- wire objects (SPEC §14.3.3) ----
User:
type: object
required: [id, role, display_name, picture_version, created_at, updated_at, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
role:
$ref: '#/components/schemas/Role'
display_name:
type: string
picture_version:
type: [integer, 'null']
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
Device:
type: object
description: Only in /me, /devices and /invites/accept.
required: [id, user_id, name, app_version_code, app_version_name, created_at, last_seen_at, revoked_at, is_current]
properties:
id:
$ref: '#/components/schemas/Uuid'
user_id:
$ref: '#/components/schemas/Uuid'
name:
type: string
app_version_code:
type: [integer, 'null']
app_version_name:
type: [string, 'null']
description: The app's `versionName` from `X-Kvitt-App-Version-Name`; null until a request carried it.
created_at:
$ref: '#/components/schemas/Timestamp'
last_seen_at:
$ref: '#/components/schemas/Timestamp'
revoked_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
is_current:
type: boolean
Invite:
type: object
description: '`code` and `link` only in the POST /invites response; null in GET /invites.'
required: [id, kind, target_user_id, code, link, created_at, expires_at]
properties:
id:
$ref: '#/components/schemas/Uuid'
kind:
$ref: '#/components/schemas/InviteKind'
target_user_id:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
code:
type: [string, 'null']
link:
type: [string, 'null']
created_at:
$ref: '#/components/schemas/Timestamp'
expires_at:
$ref: '#/components/schemas/Timestamp'
Share:
type: object
description: A share of an account default.
required: [user_id, share_bp]
properties:
user_id:
$ref: '#/components/schemas/Uuid'
share_bp:
type: integer
Account:
type: object
required: [id, name, default_shares, created_by_user_id, created_at, updated_at, archived_at, deleted_at, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
name:
type: string
default_shares:
type: array
description: Sorted by `user_id`.
minItems: 2
maxItems: 2
items:
$ref: '#/components/schemas/Share'
created_by_user_id:
$ref: '#/components/schemas/Uuid'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
archived_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
Category:
type: object
required: [id, default_key, name, icon, color, sort_order, created_at, updated_at, archived_at, deleted_at, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
default_key:
oneOf:
- $ref: '#/components/schemas/DefaultCategoryKey'
- type: 'null'
name:
type: [string, 'null']
icon:
type: string
description: An icon token of SPEC §9.7.
color:
type: string
description: A colour token of SPEC §9.7.
sort_order:
type: integer
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
archived_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
Split:
type: object
description: A split of an expense; `share_bp` null for mode exact.
required: [user_id, share_ore, share_bp]
properties:
user_id:
$ref: '#/components/schemas/Uuid'
share_ore:
$ref: '#/components/schemas/Ore'
share_bp:
type: [integer, 'null']
TemplateSplit:
type: object
description: Exactly one of `share_bp` / `share_ore` non-null.
required: [user_id, share_bp, share_ore]
properties:
user_id:
$ref: '#/components/schemas/Uuid'
share_bp:
type: [integer, 'null']
share_ore:
type: [integer, 'null']
format: int64
Expense:
type: object
required: [id, account_id, category_id, amount_ore, description, notes, expense_date, payer_user_id, split_mode, splits,
recurring_template_id, recurring_period, created_by_user_id, updated_by_user_id, created_at, updated_at,
deleted_at, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
account_id:
$ref: '#/components/schemas/Uuid'
category_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
description:
type: string
notes:
type: string
expense_date:
$ref: '#/components/schemas/Date'
payer_user_id:
$ref: '#/components/schemas/Uuid'
split_mode:
$ref: '#/components/schemas/SplitMode'
splits:
type: array
description: Sorted by `user_id`.
minItems: 2
maxItems: 2
items:
$ref: '#/components/schemas/Split'
recurring_template_id:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
recurring_period:
oneOf:
- $ref: '#/components/schemas/Period'
- type: 'null'
created_by_user_id:
$ref: '#/components/schemas/Uuid'
updated_by_user_id:
$ref: '#/components/schemas/Uuid'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
Allocation:
type: object
required: [account_id, amount_ore]
properties:
account_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
Settlement:
type: object
required: [id, from_user_id, to_user_id, amount_ore, settlement_date, note, allocations, created_by_user_id,
updated_by_user_id, created_at, updated_at, deleted_at, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
from_user_id:
$ref: '#/components/schemas/Uuid'
to_user_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
settlement_date:
$ref: '#/components/schemas/Date'
note:
type: string
allocations:
type: array
description: Sorted by `account_id`.
items:
$ref: '#/components/schemas/Allocation'
created_by_user_id:
$ref: '#/components/schemas/Uuid'
updated_by_user_id:
$ref: '#/components/schemas/Uuid'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
RecurringTemplate:
type: object
required: [id, account_id, category_id, amount_ore, description, notes, payer_user_id, split_mode, splits, day_of_month,
start_period, end_period, paused, last_generated_period, next_occurrence_date, created_by_user_id,
updated_by_user_id, created_at, updated_at, deleted_at, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
account_id:
$ref: '#/components/schemas/Uuid'
category_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
description:
type: string
notes:
type: string
payer_user_id:
$ref: '#/components/schemas/Uuid'
split_mode:
$ref: '#/components/schemas/SplitMode'
splits:
type: array
description: Sorted by `user_id`; empty for mode default.
items:
$ref: '#/components/schemas/TemplateSplit'
day_of_month:
type: integer
start_period:
$ref: '#/components/schemas/Period'
end_period:
oneOf:
- $ref: '#/components/schemas/Period'
- type: 'null'
paused:
type: boolean
last_generated_period:
oneOf:
- $ref: '#/components/schemas/Period'
- type: 'null'
next_occurrence_date:
description: Computed by the server (SPEC §9.5); null when paused, deleted or past `end_period`.
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
created_by_user_id:
$ref: '#/components/schemas/Uuid'
updated_by_user_id:
$ref: '#/components/schemas/Uuid'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
AuditChange:
type: object
description: One field of the audit diff (SPEC §14.4.7). `old` and `new` are JSON values of the field's type, or null.
required: [field, old, new]
properties:
field:
type: string
old: {}
new: {}
AuditEntry:
type: object
required: [id, entity_type, entity_id, action, source, actor_user_id, client_at, server_at, changes, server_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
entity_type:
$ref: '#/components/schemas/EntityType'
entity_id:
$ref: '#/components/schemas/Uuid'
action:
$ref: '#/components/schemas/AuditAction'
source:
$ref: '#/components/schemas/AuditSource'
actor_user_id:
$ref: '#/components/schemas/Uuid'
client_at:
$ref: '#/components/schemas/Timestamp'
server_at:
$ref: '#/components/schemas/Timestamp'
changes:
type: array
items:
$ref: '#/components/schemas/AuditChange'
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
Error:
type: object
description: '`message` is English developer text, never shown to users. `field` is the JSON key concerned or null.'
required: [code, message, field]
properties:
code:
$ref: '#/components/schemas/ErrorCode'
message:
type: string
field:
type: [string, 'null']
# ---- request and response bodies (SPEC §14.3.4) ----
Health:
type: object
required: [status, server_version, schema_version, db_ok]
properties:
status:
type: string
enum: [ok, degraded]
server_version:
type: string
schema_version:
type: integer
db_ok:
type: boolean
SetupStatus:
type: object
required: [initialized]
properties:
initialized:
type: boolean
AcceptInviteRequest:
type: object
required: [code, device_name]
properties:
code:
type: string
description: Normalised per SPEC §8.3 by the app before sending.
device_name:
type: string
display_name:
type: string
description: Required for `admin_setup` and `partner` invites (`invalid_request` if missing), ignored for `device`.
AcceptInviteResponse:
type: object
required: [token, invite_kind, user, device, household]
properties:
token:
type: string
description: '`kvt_` + base64url without padding of 32 random bytes (47 characters, SPEC §8.2).'
invite_kind:
$ref: '#/components/schemas/InviteKind'
description: The kind of the accepted invite; the app shows the profile step only after `partner` (AC-2.10).
user:
$ref: '#/components/schemas/User'
device:
$ref: '#/components/schemas/Device'
household:
type: array
items:
$ref: '#/components/schemas/User'
ApkLatest:
type: object
description: '`latest.json` (SPEC §10.6).'
required: [version_code, version_name, file_name, size_bytes, sha256, released_at]
properties:
version_code:
type: integer
version_name:
type: string
file_name:
type: string
size_bytes:
type: integer
format: int64
sha256:
type: string
released_at:
$ref: '#/components/schemas/Timestamp'
MeResponse:
type: object
required: [user, device, household, public_url]
properties:
user:
$ref: '#/components/schemas/User'
device:
$ref: '#/components/schemas/Device'
household:
type: array
items:
$ref: '#/components/schemas/User'
public_url:
type: string
PatchMeRequest:
type: object
minProperties: 1
properties:
display_name:
type: string
CreateInviteRequest:
type: object
required: [kind]
properties:
kind:
type: string
enum: [partner, device]
target_user_id:
# TODO(spec): §14.3.4 E-12 writes `uuid?` in a request; read here as nullable and optional (absent = null).
description: Required for `device`, must be null for `partner`.
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
InviteList:
type: object
required: [invites]
properties:
invites:
type: array
items:
$ref: '#/components/schemas/Invite'
DeviceList:
type: object
required: [devices]
properties:
devices:
type: array
items:
$ref: '#/components/schemas/Device'
PushTokenRequest:
type: object
# TODO(spec): §14.3.4 E-17 writes `string?`; read here as required and nullable (null clears), so an empty body is invalid_request.
required: [fcm_token]
properties:
fcm_token:
type: [string, 'null']
CreateAccountRequest:
type: object
required: [name, default_shares]
properties:
name:
type: string
default_shares:
type: array
minItems: 2
maxItems: 2
items:
$ref: '#/components/schemas/Share'
PatchAccountRequest:
type: object
minProperties: 1
properties:
name:
type: string
default_shares:
type: array
minItems: 2
maxItems: 2
items:
$ref: '#/components/schemas/Share'
CreateCategoryRequest:
type: object
required: [name, icon, color]
properties:
name:
type: string
icon:
type: string
color:
type: string
PatchCategoryRequest:
type: object
minProperties: 1
properties:
name:
type: [string, 'null']
description: null resets a default category's name (`category_is_default` on a non-default category).
icon:
type: string
color:
type: string
sort_order:
type: integer
CreateRecurringTemplateRequest:
type: object
description: |
`splits` per SPEC §9.1: none for `default`; two bp entries for `ratio` and `full`; two öre entries summing to the
amount for `exact`.
required: [account_id, category_id, amount_ore, description, notes, payer_user_id, split_mode, splits, day_of_month,
start_period, end_period, paused]
properties:
account_id:
$ref: '#/components/schemas/Uuid'
category_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
description:
type: string
notes:
type: string
payer_user_id:
$ref: '#/components/schemas/Uuid'
split_mode:
$ref: '#/components/schemas/SplitMode'
splits:
type: array
items:
$ref: '#/components/schemas/TemplateSplit'
day_of_month:
type: integer
start_period:
$ref: '#/components/schemas/Period'
end_period:
oneOf:
- $ref: '#/components/schemas/Period'
- type: 'null'
paused:
type: boolean
PatchRecurringTemplateRequest:
type: object
description: Any subset of the E-28 fields; `split_mode` and `splits` must be sent together.
minProperties: 1
dependentRequired:
split_mode: [splits]
splits: [split_mode]
properties:
account_id:
$ref: '#/components/schemas/Uuid'
category_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
description:
type: string
notes:
type: string
payer_user_id:
$ref: '#/components/schemas/Uuid'
split_mode:
$ref: '#/components/schemas/SplitMode'
splits:
type: array
items:
$ref: '#/components/schemas/TemplateSplit'
day_of_month:
type: integer
start_period:
$ref: '#/components/schemas/Period'
end_period:
oneOf:
- $ref: '#/components/schemas/Period'
- type: 'null'
paused:
type: boolean
ProposalRequest:
type: object
required: [payer_user_id, amount_ore]
properties:
payer_user_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
exclude_settlement_id:
# TODO(spec): §14.3.4 E-33 writes `uuid?` in a request; read here as nullable and optional (absent = null).
description: The settlement being edited, excluded from the balances.
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
ProposalResponse:
type: object
required: [allocations, excess_ore, over_settled_account_ids]
properties:
allocations:
type: array
description: In the order produced by SPEC §9.4.
items:
$ref: '#/components/schemas/Allocation'
excess_ore:
$ref: '#/components/schemas/Ore'
over_settled_account_ids:
type: array
items:
$ref: '#/components/schemas/Uuid'
# E-34 and E-35 (D-133, SPEC §14.5.7)
TrendPoint:
type: object
required: [month, amount_ore, moving_average_ore]
properties:
month:
$ref: '#/components/schemas/Period'
amount_ore:
$ref: '#/components/schemas/Ore'
moving_average_ore:
$ref: '#/components/schemas/Ore'
description: Trailing over 3 months; the first points average what exists (D-128).
Trend:
type: object
required: [total_ore, average_ore, last_month_ore, last_month_vs_average_pct, current_month_ore, points]
properties:
total_ore:
$ref: '#/components/schemas/Ore'
description: Sum over the whole months.
average_ore:
$ref: '#/components/schemas/Ore'
description: '`total_ore` ÷ months, half-up to öre (empty months count).'
last_month_ore:
$ref: '#/components/schemas/Ore'
description: The last whole month (the month before the current one).
last_month_vs_average_pct:
type: [integer, 'null']
description: '`last_month_ore` against `average_ore`, whole percent of the average, half-up; null when the average is 0.'
current_month_ore:
$ref: '#/components/schemas/Ore'
description: The current month from its first day to today (Europe/Stockholm); not one of the whole months.
points:
type: array
description: One point per whole month, oldest first, empty months as zero.
items:
$ref: '#/components/schemas/TrendPoint'
CategoryTrend:
type: object
required: [category_id, trend]
properties:
category_id:
$ref: '#/components/schemas/Uuid'
trend:
$ref: '#/components/schemas/Trend'
TrendsResponse:
type: object
required: [measure, today, current_month, months, household, categories]
properties:
measure:
$ref: '#/components/schemas/AnalyticsMeasure'
today:
$ref: '#/components/schemas/Date'
description: Today in Europe/Stockholm.
current_month:
$ref: '#/components/schemas/Period'
months:
type: integer
description: The number of whole months in every series.
household:
$ref: '#/components/schemas/Trend'
categories:
type: array
description: Every category with spending in the whole months or the current month (archived and deleted
included), by `total_ore` descending, then `category_id`.
items:
$ref: '#/components/schemas/CategoryTrend'
ForecastMonth:
type: object
required: [month, recurring_ore, baseline_ore, forecast_ore]
properties:
month:
$ref: '#/components/schemas/Period'
recurring_ore:
$ref: '#/components/schemas/Ore'
baseline_ore:
$ref: '#/components/schemas/Ore'
forecast_ore:
$ref: '#/components/schemas/Ore'
description: '`recurring_ore + baseline_ore`.'
AccountForecast:
type: object
required: [account_id, current_month_ore, months]
properties:
account_id:
$ref: '#/components/schemas/Uuid'
current_month_ore:
$ref: '#/components/schemas/Ore'
description: The current month so far, recurring-generated expenses included.
months:
type: array
description: The two months after the current one, in order.
items:
$ref: '#/components/schemas/ForecastMonth'
ForecastResponse:
type: object
required: [measure, today, current_month, accounts]
properties:
measure:
$ref: '#/components/schemas/AnalyticsMeasure'
today:
$ref: '#/components/schemas/Date'
description: Today in Europe/Stockholm.
current_month:
$ref: '#/components/schemas/Period'
accounts:
type: array
description: Every non-deleted account, archived included, by `created_at`, then `account_id`.
items:
$ref: '#/components/schemas/AccountForecast'
# ---- sync (SPEC §14.4) ----
SyncPullResponse:
type: object
required: [epoch, since, next_since, has_more, server_time, users, categories, accounts, recurring_templates, expenses,
settlements, audit_entries]
properties:
epoch:
type: string
description: '`meta.sync_epoch`, 32 lowercase hex characters.'
pattern: '^[0-9a-f]{32}$'
since:
type: integer
format: int64
next_since:
type: integer
format: int64
description: The highest `server_version` in the page, or `since` when the page is empty.
has_more:
type: boolean
server_time:
$ref: '#/components/schemas/Timestamp'
users:
type: array
items:
$ref: '#/components/schemas/User'
categories:
type: array
items:
$ref: '#/components/schemas/Category'
accounts:
type: array
items:
$ref: '#/components/schemas/Account'
recurring_templates:
type: array
items:
$ref: '#/components/schemas/RecurringTemplate'
expenses:
type: array
items:
$ref: '#/components/schemas/Expense'
settlements:
type: array
items:
$ref: '#/components/schemas/Settlement'
audit_entries:
type: array
items:
$ref: '#/components/schemas/AuditEntry'
PushedExpense:
type: object
description: |
Exactly the client-writable fields (SPEC §14.4.3). Server-owned fields are ignored if sent. A delete is the full
entity with `deleted_at` set.
required: [id, account_id, category_id, amount_ore, description, notes, expense_date, payer_user_id, split_mode, splits,
created_at, updated_at, deleted_at]
properties:
id:
$ref: '#/components/schemas/Uuid'
account_id:
$ref: '#/components/schemas/Uuid'
category_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
description:
type: string
notes:
type: string
expense_date:
$ref: '#/components/schemas/Date'
payer_user_id:
$ref: '#/components/schemas/Uuid'
split_mode:
$ref: '#/components/schemas/SplitMode'
splits:
type: array
items:
$ref: '#/components/schemas/Split'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
PushedSettlement:
type: object
description: Exactly the client-writable fields (SPEC §14.4.3).
required: [id, from_user_id, to_user_id, amount_ore, settlement_date, note, allocations, created_at, updated_at, deleted_at]
properties:
id:
$ref: '#/components/schemas/Uuid'
from_user_id:
$ref: '#/components/schemas/Uuid'
to_user_id:
$ref: '#/components/schemas/Uuid'
amount_ore:
$ref: '#/components/schemas/Ore'
settlement_date:
$ref: '#/components/schemas/Date'
note:
type: string
allocations:
type: array
items:
$ref: '#/components/schemas/Allocation'
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
ExpenseMutation:
type: object
required: [mutation_id, entity_type, entity]
properties:
mutation_id:
$ref: '#/components/schemas/Uuid'
entity_type:
const: expense
entity:
$ref: '#/components/schemas/PushedExpense'
SettlementMutation:
type: object
required: [mutation_id, entity_type, entity]
properties:
mutation_id:
$ref: '#/components/schemas/Uuid'
entity_type:
const: settlement
entity:
$ref: '#/components/schemas/PushedSettlement'
Mutation:
oneOf:
- $ref: '#/components/schemas/ExpenseMutation'
- $ref: '#/components/schemas/SettlementMutation'
discriminator:
propertyName: entity_type
mapping:
expense: '#/components/schemas/ExpenseMutation'
settlement: '#/components/schemas/SettlementMutation'
SyncPushRequest:
type: object
required: [mutations]
properties:
mutations:
type: array
description: 1 to 100 mutations (more is 422 `batch_too_large`).
items:
$ref: '#/components/schemas/Mutation'
AcceptedResult:
type: object
# TODO(spec): §14.3.1 says optional response fields are present with null, but the §14.4.3 example gives accepted and
# rejected results disjoint key sets; the example's key sets are used here.
required: [mutation_id, entity_type, entity_id, status, server_version, entity]
properties:
mutation_id:
$ref: '#/components/schemas/Uuid'
entity_type:
$ref: '#/components/schemas/EntityType'
entity_id:
$ref: '#/components/schemas/Uuid'
status:
const: accepted
server_version:
$ref: '#/components/schemas/ServerVersionNumber'
entity:
description: The full wire object after processing, of the type named by `entity_type`.
oneOf:
- $ref: '#/components/schemas/Expense'
- $ref: '#/components/schemas/Settlement'
RejectedResult:
type: object
required: [mutation_id, entity_type, entity_id, status, reason, message, current]
properties:
mutation_id:
$ref: '#/components/schemas/Uuid'
entity_type:
$ref: '#/components/schemas/EntityType'
entity_id:
$ref: '#/components/schemas/Uuid'
status:
const: rejected
reason:
$ref: '#/components/schemas/RejectionReason'
message:
type: string
description: For validation failures, the domain error token of SPEC §14.5.3.
current:
description: The stored row as a full wire object of the type named by `entity_type`, or null when the server has no row with that id.
oneOf:
- $ref: '#/components/schemas/Expense'
- $ref: '#/components/schemas/Settlement'
- type: 'null'
MutationResult:
oneOf:
- $ref: '#/components/schemas/AcceptedResult'
- $ref: '#/components/schemas/RejectedResult'
discriminator:
propertyName: status
mapping:
accepted: '#/components/schemas/AcceptedResult'
rejected: '#/components/schemas/RejectedResult'
SyncPushResponse:
type: object
required: [server_time, results]
properties:
server_time:
$ref: '#/components/schemas/Timestamp'
results:
type: array
description: One result per mutation, in request order.
items:
$ref: '#/components/schemas/MutationResult'