Contents

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 quantity

Credentials 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 edit

The 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-openapi

Omit --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 test

The 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:

  • Confirmation and ReceiptItem still 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 Output switch changed. forbidden and internalServerError are new on

nearly every operation, and tooManyRequests is gone from most of them.

  • Ten POST operations take an idempotenceKey header. Previously a middleware

invented one per request, which never actually made a retry idempotent.

  • Six PaymentMethodData variants 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, not Decimal.
  • SwiftPM reports openapi.yaml and openapi-generator-config.yaml as 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