Files
kvitt/contracts/openapi.yaml
T
jacobh2 671c00d0d8
Build Kvitt images / build (push) Failing after 8m17s
K1: card purchases on the server (schema 4, Enable Banking adapter, E-36 to E-44)
Contract (SPEC §14.8): migration 0004_card_purchases.sql (server schema 4, byte-identical
contract and server copies), E-36 to E-44 in contracts/openapi.yaml with the four new error
codes and a 502 response, examples for every card body and error code, the
match_card_purchase vectors (17 cases) with their Python reference in tools/contracts-check.py
(157 checks), web/src/api/schema.d.ts regenerated; web/src/lib/vectors.test.ts lists
match_card_purchase as not ported until K3 ports it (orchestrator's allowance).

Server: src/cards/ with the provider registry (CP-1), the CardSource trait and its
Enable Banking adapter (JWT RS256 kid = application id, /aspsps, /auth, /sessions,
transactions with PSU headers for user-present fetches, CP-14 error mapping, CP-7 mapping),
the windowed fetch (30-day windows newest first, split at the 100-row cap down to a day,
de-duplication by provider_ref, continuation keys, WRONG_TRANSACTIONS_PERIOD ends the walk),
CP-8 matching with the secondary rule, the background poller (06:45, 11:45, 16:45, 21:45,
4 per 24 hours, 2-hour skip, no backlog), the bilingual callback page (E-39), E-36 to E-38
and E-40 to E-44, link clearing in sync push step 7, attempt pruning at 04:00, the card_fetch
log line with aggregates only, KVITT_EB_* configuration. Tests against a local stub
(tests/cards_api/, 34 tests); the real API is never called.

Infra: Pi compose mounts secrets/cards read-only and passes the two application ids;
.env.example keys; dev stack stages infra/secrets/cards/; NPM block with the /cards/callback
gate line and unlogged location (kvitt.conf and README step 4); README "Card purchases".

Pre-commit hook bypassed (--no-verify, orchestrator's decision): the Android fast tests fail
in :domain VectorFilesTest (and the full check in :data ExampleRoundTripTest) because they pin
the contract's vector and example sets; matchCardPurchase and the card DTOs are K2's by design
(contract-first landing; K2 lands next). Server, contracts-check and web gates are green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-24 16:43:26 +02:00

3408 lines
117 KiB
YAML
Raw 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, and a device of the other user is 404 `not_found`, the same answer as an unknown id (SPEC §8.4, D-156).
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'
'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'
/cards/providers:
get:
operationId: listCardProviders
summary: E-36 Card catalogue
description: The supported cards in registry order (SPEC §9.12 CP-1). `available` is false when the server has no card application for the caller (CP-13).
parameters:
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The catalogue.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardProviderList'
'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'
/cards/auth:
post:
operationId: createCardAuth
summary: E-37 Start connecting or renewing a card
description: |
Creates an authorisation attempt (SPEC §9.12 CP-2) and returns the bank URL the client opens. A pending older
attempt of the caller for the same provider expires.
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/CreateCardAuthRequest'
responses:
'201':
description: The pending attempt with `auth_url`.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardAttempt'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found` (unknown provider).'
'409':
$ref: '#/components/responses/Conflict'
description: '`cards_unavailable`.'
'422':
$ref: '#/components/responses/Unprocessable'
description: '`validation_failed` (`history_days`).'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'502':
$ref: '#/components/responses/BadGateway'
'503':
$ref: '#/components/responses/Unavailable'
/cards/auth/{attempt_id}:
get:
operationId: getCardAuth
summary: E-38 Poll an authorisation attempt
description: A pending attempt past `expires_at` is answered and stored as `expired` (SPEC §9.12 CP-2). `auth_url` is null.
parameters:
- $ref: '#/components/parameters/AttemptId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The attempt.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardAttempt'
'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'
/cards/callback:
get:
operationId: getCardCallback
summary: E-39 Card connection callback page (HTML)
description: |
The bank's redirect after BankID (SPEC §9.12 CP-2). Always 200 HTML, success and failure alike;
`Cache-Control: no-store`, `Referrer-Policy: no-referrer`. Swedish when `Accept-Language` prefers `sv`, English
otherwise. A malformed query answers the failure page.
security: []
parameters:
- name: code
in: query
required: false
description: The one-time authorisation code.
schema:
type: string
- name: state
in: query
required: false
description: The attempt's state.
schema:
type: string
- name: error
in: query
required: false
description: Set by the bank when the user cancelled.
schema:
type: string
- $ref: '#/components/parameters/ProtocolOptional'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The callback 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'
/cards/sync:
get:
operationId: cardSync
summary: E-40 Pull the caller's card rows since a cursor
description: |
The caller's connections and purchases with `card_version > since`, tombstones included, counted together and
ordered by `card_version` (SPEC §14.3.4, like §14.4.2).
parameters:
- name: since
in: query
required: true
description: The highest `card_version` the client 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.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardSyncResponse'
'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'
/cards/connections/{connection_id}:
patch:
operationId: patchCardConnection
summary: E-41 Shorten the history setting
description: |
Only a value of 7, 30, 90 or 180 at most the current one (SPEC §9.12 CP-3); a longer one is a renewal through
E-37. Stored purchases stay.
parameters:
- $ref: '#/components/parameters/ConnectionId'
- $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/PatchCardConnectionRequest'
responses:
'200':
description: The connection.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardConnection'
'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: deleteCardConnection
summary: E-43 Disconnect a card
description: |
Revokes the session at the bank (best effort), sets `disconnected`, deletes the purchases neither linked nor
ignored and, with `erase_handled=true`, the linked and ignored ones too (SPEC §9.12 CP-12). Expenses stay.
Idempotent on a disconnected connection.
parameters:
- $ref: '#/components/parameters/ConnectionId'
- name: erase_handled
in: query
required: false
description: Also delete linked and ignored purchases. Default false.
schema:
type: boolean
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The disconnected connection.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardConnection'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'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'
/cards/connections/{connection_id}/refresh:
post:
operationId: refreshCardConnection
summary: E-42 Fetch purchases with the user present
description: |
Contacts the bank at most once per connection per 60 seconds and waits up to 25 seconds (SPEC §9.12 CP-5).
`fetched` is false when the 60-second rule answered without contacting the bank or the fetch is still running
(then `connection.fetching` is true). A failed fetch is recorded in `last_fetch_error` and still answers 200.
parameters:
- $ref: '#/components/parameters/ConnectionId'
- $ref: '#/components/parameters/Protocol'
- $ref: '#/components/parameters/AppVersion'
- $ref: '#/components/parameters/AppVersionName'
- $ref: '#/components/parameters/AcceptLanguage'
responses:
'200':
description: The connection after the fetch.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardRefreshResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found`.'
'409':
$ref: '#/components/responses/Conflict'
description: '`card_consent_expired`.'
'426':
$ref: '#/components/responses/ProtocolUnsupported'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/Internal'
'503':
$ref: '#/components/responses/Unavailable'
/cards/purchases/{purchase_id}:
put:
operationId: putCardPurchase
summary: E-44 Link, unlink or ignore a card purchase
description: |
Sets the link and the ignore flag (SPEC §9.12 CP-9, §9.6). The unchanged row with its existing `card_version`
when nothing changes.
parameters:
- $ref: '#/components/parameters/PurchaseId'
- $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/PutCardPurchaseRequest'
responses:
'200':
description: The purchase.
headers:
X-Kvitt-Server-Version:
$ref: '#/components/headers/ServerVersion'
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/CardPurchase'
'400':
$ref: '#/components/responses/BadRequest'
description: '`invalid_request`.'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
description: '`not_found` (the purchase, or with `field` `expense_id` an unknown or deleted expense).'
'409':
$ref: '#/components/responses/Conflict'
description: '`expense_already_linked`.'
'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'
AttemptId:
name: attempt_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
ConnectionId:
name: connection_id
in: path
required: true
description: Not a UUID is 404 `not_found`.
schema:
$ref: '#/components/schemas/Uuid'
PurchaseId:
name: purchase_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'
BadGateway:
description: '`card_bank_unavailable` (the card service or the bank did not answer or refused).'
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
- cards_unavailable
- card_consent_expired
- expense_already_linked
- payload_too_large
- unsupported_media_type
- validation_failed
- image_invalid
- batch_too_large
- protocol_unsupported
- rate_limited
- internal
- card_bank_unavailable
- 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'
# ---- card purchases (SPEC §9.12, §14.3.3, D-158 to D-180) ----
CardConnectionStatus:
type: string
enum: [active, expired, disconnected]
CardFetchError:
type: string
description: CP-14.
enum: [bank_unavailable, rate_limited, consent_expired, failed]
CardPurchaseStatus:
type: string
enum: [pending, booked]
CardDirection:
type: string
enum: [debit, credit]
CardAttemptStatus:
type: string
enum: [pending, connected, failed, expired]
CardClient:
type: string
enum: [android, web]
CardProvider:
type: object
description: An entry of the card catalogue (CP-1). `available`, the server can connect it for the caller (CP-13).
required: [id, name, issuer, image_key, available]
properties:
id:
type: string
description: Open string, never an enum.
name:
type: string
issuer:
type: string
image_key:
type: string
available:
type: boolean
CardProviderList:
type: object
required: [providers]
properties:
providers:
type: array
items:
$ref: '#/components/schemas/CardProvider'
CardConnection:
type: object
description: Only ever sent to its own user's devices. No session id, account uid or provider reference.
required: [id, provider_id, status, history_days, valid_until, last_fetch_at, last_fetch_error, fetching, created_at,
updated_at, card_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
provider_id:
type: string
status:
$ref: '#/components/schemas/CardConnectionStatus'
history_days:
type: integer
valid_until:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
last_fetch_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
last_fetch_error:
oneOf:
- $ref: '#/components/schemas/CardFetchError'
- type: 'null'
fetching:
type: boolean
description: A fetch for it is running at the time of the response.
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
card_version:
type: integer
format: int64
description: From `card_sequence` (SPEC §14.1.3), not the household's `server_version`.
CardPurchase:
type: object
description: A tombstone has `deleted_at` set and its content erased (SPEC §14.1.3).
required: [id, connection_id, status, direction, purchase_date, booking_date, amount_ore, original_amount,
original_currency, merchant, expense_id, ignored, created_at, updated_at, deleted_at, card_version]
properties:
id:
$ref: '#/components/schemas/Uuid'
connection_id:
$ref: '#/components/schemas/Uuid'
status:
$ref: '#/components/schemas/CardPurchaseStatus'
direction:
$ref: '#/components/schemas/CardDirection'
purchase_date:
$ref: '#/components/schemas/Date'
booking_date:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
amount_ore:
type: [integer, 'null']
format: int64
description: Null for a purchase in another currency than SEK and for a tombstone.
original_amount:
type: [string, 'null']
description: The bank's decimal amount, for a purchase in another currency than SEK.
original_currency:
type: [string, 'null']
description: ISO 4217, for a purchase in another currency than SEK.
merchant:
type: string
expense_id:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
ignored:
type: boolean
created_at:
$ref: '#/components/schemas/Timestamp'
updated_at:
$ref: '#/components/schemas/Timestamp'
deleted_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
card_version:
type: integer
format: int64
CardAttempt:
type: object
description: '`auth_url` only in the E-37 response (null in E-38); `connection` once connected.'
required: [id, provider_id, status, failure, auth_url, expires_at, connection]
properties:
id:
$ref: '#/components/schemas/Uuid'
provider_id:
type: string
status:
$ref: '#/components/schemas/CardAttemptStatus'
failure:
type: [string, 'null']
description: '`cancelled`, `no_account` or `bank_error` for a failed attempt, else null.'
auth_url:
type: [string, 'null']
expires_at:
$ref: '#/components/schemas/Timestamp'
connection:
oneOf:
- $ref: '#/components/schemas/CardConnection'
- type: 'null'
CreateCardAuthRequest:
type: object
required: [provider_id, history_days, client]
properties:
provider_id:
type: string
history_days:
type: integer
description: One of 7, 30, 90, 180 (SPEC §9.6).
client:
$ref: '#/components/schemas/CardClient'
CardSyncResponse:
type: object
required: [since, next_since, has_more, server_time, connections, purchases]
properties:
since:
type: integer
format: int64
next_since:
type: integer
format: int64
description: The highest `card_version` in the page, or `since` when the page is empty.
has_more:
type: boolean
server_time:
$ref: '#/components/schemas/Timestamp'
connections:
type: array
items:
$ref: '#/components/schemas/CardConnection'
purchases:
type: array
items:
$ref: '#/components/schemas/CardPurchase'
PatchCardConnectionRequest:
type: object
required: [history_days]
properties:
history_days:
type: integer
description: One of 7, 30, 90, 180, at most the current value (CP-3).
CardRefreshResponse:
type: object
required: [connection, fetched]
properties:
connection:
$ref: '#/components/schemas/CardConnection'
fetched:
type: boolean
description: False when the 60-second rule answered without contacting the bank, or the fetch is still running.
PutCardPurchaseRequest:
type: object
required: [expense_id, ignored]
properties:
expense_id:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
ignored:
type: boolean