a86ad241f1
Kvitt is served on the default HTTPS port through Nginx Proxy Manager, so the OpenAPI server is https://{host} (contracts/openapi.yaml and openapi.rs PUBLIC_SERVER_URL), examples E-7 public_url and E-12 link are https://kvitt.example.invalid without a port, tools/contracts-check.py expects the portless header, and the server's tests, config message and README name no port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2539 lines
85 KiB
YAML
2539 lines
85 KiB
YAML
# 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'
|
||
|
||
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]
|
||
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'
|
||
|
||
# ---- 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'
|