laconicman/yoomoneyapiclient
A Swift client for the YooKassa API, generated with
Documentation
The rendered DocC catalog is authoritative for architecture and direction; when it and this README disagree, believe the catalog. Build it with swift package generate-documentation --target YooMoneyAPI, or read the sources:
| Article | What it answers | |---|---| | Design | Why the client is generated, and every load-bearing decision with the alternative that was rejected | | Migrating from 1.x to 2.0 | Every source-breaking change, the exact edit that resolves it, and why it was made | | The Specification Pipeline | Why an official specification still has to be rewritten before a generator can use it, and what the four transforms do | | Tech Debt | Every compromise carried, what it costs, and what would retire it | | Roadmap | Planned work in priority order |
Installation
.package(url: "https://github.com/laconicman/YooMoneyAPIClient", from: "2.0.0")Requires Swift 6.1 or newer, macOS 13 / iOS 16 / tvOS 16 / watchOS 9 / visionOS 1.
Usage
import Foundation
import YooMoneyAPI
let client = try YooClient(credentials: .init(username: "<shopId>", password: "<secretKey>"))
let response = try await client.createPayment(
headers: .init(idempotenceKey: UUID().uuidString),
body: .json(
.init(
amount: .init(value: 1924.0, currency: .rub),
description: "Order #42",
confirmation: .redirect(returnUrl: "https://example.com/return")
)
)
)
switch response {
case .ok(let ok):
let payment = try ok.body.json
print(payment.id, payment.status)
case .badRequest, .unauthorized, .forbidden, .internalServerError, .undocumented:
// Handle per case; the generated enum is exhaustive.
break
}Idempotence keys
Every POST takes a required idempotenceKey header. Pass one key per logical operation and reuse it when retrying that operation, which is what makes a retry safe. A fresh key on every attempt — for instance one generated inside a middleware — defeats the mechanism, so the client does not generate keys for you.
Amounts
The API expresses money as decimal strings. Components.Schemas.MonetaryAmount has a Double initializer, and Double.string / String.double convert in either direction using the format the API expects.
let total = try Components.Schemas.MonetaryAmount.from(items: cart) // sums price x quantityCredentials for debugging
Credentials.environment reads APP_USERNAME and APP_PASSWORD and returns nil when either is unset. In Xcode, set them under Edit scheme. Keep them out of version control.
Specification pipeline
Spec/upstream/yookassa-openapi-specification.yaml pristine upstream snapshot
│ swift run SpecSync
▼
Sources/YooMoneyAPIClient/openapi.yaml derived — do not edit
│ swift package generate-code-from-openapi
▼
Sources/GeneratedSources/{Types,Client}.swift generated — do not editThe upstream document is vendored verbatim and never edited; openapi.yaml is derived from it. Fed to the generator unchanged, upstream produces a client that cannot decode a payment response, so Tools/SpecSync applies four transforms — repointing discriminated unions, collapsing annotation-only allOf wrappers, hoisting inline bodies, and injecting operationIds. Each rule fails the sync rather than guess when the upstream shape it relies on changes, because a client that is quietly wrong is worse than none.
The Specification Pipeline explains each rule, why an official specification needs rewriting at all, and where the defects come from.
Refresh from upstream
% swift run SpecSync --fetch # download a new snapshot, then re-derive openapi.yaml
% swift package generate-code-from-openapiOmit --fetch to re-derive from the committed snapshot. Review both diffs: the snapshot diff is upstream's change, the openapi.yaml diff is its effect on the client.
Endpoint scope
openapi.yaml keeps every upstream operation; filter.paths in Sources/YooMoneyAPIClient/openapi-generator-config.yaml decides which reach the client. Currently: payments, payment methods, invoices, refunds, receipts, deals, shop settings and POS links. Payouts, personal data, SBP participants and webhooks are excluded because they need an OAuth token or a payout gateway rather than shop credentials — adding them is a change to that list alone.
Testing
% swift testThe offline suites need no credentials and cover amount arithmetic and the discriminated union decoding described above. LiveClientTests talks to the real API and is skipped unless APP_USERNAME and APP_PASSWORD are set.
Migrating from 1.x
Generated type names now follow the official specification. Deprecated aliases in Deprecations.swift cover most renames, so the compiler names the replacement for you. Four changes cannot be aliased and need reading:
ConfirmationandReceiptItemstill exist but changed meaning. They are now the
response shapes; requests use ConfirmationData and ReceiptDataItem. No alias can express that, so these surface as type mismatches at the call site.
- Every
Outputswitch changed.forbiddenandinternalServerErrorare new on
nearly every operation, and tooManyRequests is gone from most of them.
- Ten
POSToperations take anidempotenceKeyheader. Previously a middleware
invented one per request, which never actually made a retry idempotent.
- Six
PaymentMethodDatavariants no longer exist upstream — Alfa-Bank, Apple Pay,
Google Pay, QIWI, WebMoney and installments — and have no replacement.
Migrating from 1.x to 2.0 covers all of it symbol by symbol, with the reasoning behind each change and a checklist to work through.
Known limitations
Tracked in full, with costs and discharge conditions, in Tech Debt. The ones most likely to affect a caller:
- Variants that add no fields of their own (
ConfirmationExternal,PaymentMethodDataSbp)
keep the generator's value1 / value2 split. Types+.swift provides factory methods (.redirect(returnUrl:), .bankCard(card:)) that hide it for the common cases.
- Money is
Double, notDecimal. - SwiftPM reports
openapi.yamlandopenapi-generator-config.yamlas unhandled files.
That is expected: the generator plugin finds them by scanning the target's files.
Package Metadata
Repository: laconicman/yoomoneyapiclient
Default branch: main
README: README.md