# The Payment Queue & Transactions

All purchasing in OnsideKit goes through a single, process-wide **payment queue**. It processes purchases and restores, and keeps a list of transactions your app must handle. The model is intentionally close to StoreKit's `SKPaymentQueue`.

## Accessing the queue

```swift
@MainActor static func defaultPaymentQueue() -> OnsidePaymentQueue
```

```swift
let queue = Onside.defaultPaymentQueue()
```

The queue is a shared singleton. You must call [`Onside.initialize()`](/sdk/getting-started/initialization.md) before accessing it.

{% hint style="info" %}
The accessor is `Onside.defaultPaymentQueue()`. There is no `Onside.paymentQueue()`.
{% endhint %}

`OnsidePaymentQueue` is a protocol — all of its members are `@MainActor`:

```swift
protocol OnsidePaymentQueue: AnyObject {
    var delegate: OnsidePaymentQueueDelegate? { get set }
    var storefront: OnsideStorefront? { get }
    var transactions: [OnsidePaymentTransaction] { get }
    var transactionObservers: [OnsidePaymentTransactionObserver] { get }

    func add(observer: OnsidePaymentTransactionObserver)
    func remove(observer: OnsidePaymentTransactionObserver)

    func add(
        _ payment: OnsidePayment,
        completion: ((Result<Void, OnsidePaymentQueueAddProductError>) -> Void)?
    )
    func restoreCompletedTransactions(
        completion: ((Result<Void, OnsidePaymentQueueRequestRestoreError>) -> Void)?
    )
    func finishTransaction(_ transaction: OnsidePaymentTransaction)
}
```

## A transaction observer is required

The queue makes progress only while at least one **transaction observer** is registered. Until you add one, the queue stays idle — payments and restores are accepted but nothing is processed.

```swift
Onside.defaultPaymentQueue().add(observer: myObserver)
```

Register your observer as early as possible — ideally in your `AppDelegate` — so the queue can immediately process any transactions (including unfinished ones carried over from a previous launch).

{% hint style="info" %}
**Two preconditions for processing.** The queue runs only when (1) at least one observer is registered **and** (2) a storefront exists (the user is logged in). With no observer or no session, transactions sit and wait. See [Authentication & User Account](/sdk/core-concepts/authentication.md).
{% endhint %}

When you add an observer and the queue already holds transactions, that observer is **immediately called** with the current transactions, so you can resume or finish them.

## The transaction lifecycle

Each transaction is an `OnsidePaymentTransaction` whose `transactionState` moves through these public states:

<table><thead><tr><th width="150">State</th><th>Meaning</th><th>Your action</th></tr></thead><tbody><tr><td><code>.purchasing</code></td><td>In flight — created, awaiting payment, etc.</td><td>Wait (optionally show a spinner).</td></tr><tr><td><code>.purchased</code></td><td>Bought successfully.</td><td>Unlock content, then <code>finishTransaction</code>.</td></tr><tr><td><code>.restored</code></td><td>Returned by <code>restoreCompletedTransactions</code>.</td><td>Unlock content, then <code>finishTransaction</code>.</td></tr><tr><td><code>.failed</code></td><td>Failed. <code>transaction.error</code> is set.</td><td><code>finishTransaction</code> to remove it.</td></tr></tbody></table>

```mermaid
stateDiagram-v2
    [*] --> purchasing: add(payment) / restore
    purchasing --> purchased: success
    purchasing --> restored: restored
    purchasing --> failed: failure
    purchased --> [*]: finishTransaction
    restored --> [*]: finishTransaction
    failed --> [*]: finishTransaction
```

State changes are delivered to your observer's `onsidePaymentQueue(_:updatedTransactions:)`. Inspect each transaction's `transactionState` and react. Because the public enum is resilient, always include an `@unknown default` in your `switch`:

```swift
func onsidePaymentQueue(
    _ queue: OnsidePaymentQueue,
    updatedTransactions: [OnsidePaymentTransaction]
) {
    for transaction in updatedTransactions {
        switch transaction.transactionState {
        case .purchased, .restored:
            unlockContent(for: transaction.payment.product.productIdentifier)
            queue.finishTransaction(transaction)
        case .failed:
            queue.finishTransaction(transaction)
        case .purchasing:
            break
        @unknown default:
            break
        }
    }
}
```

`.purchased` vs `.restored` only tells you how the transaction was surfaced (a fresh purchase vs. a result of `restoreCompletedTransactions`). Both grant the same entitlement and both must be finished. For the full shape of a transaction, see the [Models reference](/sdk/reference/models.md).

## Finishing transactions is mandatory

{% hint style="danger" %}
**You must call `finishTransaction(_:)` for every transaction that reaches `.purchased`, `.restored`, or `.failed`.**

If you don't, OnsideKit treats the transaction as unprocessed: it stays in the queue and is re-delivered to your observer on the next launch — which can cause you to unlock content repeatedly.
{% endhint %}

```swift
Onside.defaultPaymentQueue().finishTransaction(transaction)
```

Finishing a transaction removes it from the queue (for a purchase, this also performs the necessary server-side completion). When it is removed, observers receive `onsidePaymentQueue(_:removedTransactions:)`.

## Transactions can appear without an explicit purchase

OnsideKit reconciles the account's purchases with the server. As a result, your observer may receive transactions you didn't start in this session — for example a [subscription](/sdk/products-and-subscriptions/subscriptions.md) renewal, a purchase made on another device, or one interrupted before it finished. Treat every transaction your observer delivers the same way: act on its state and finish it.

## The observer protocol

```swift
protocol OnsidePaymentTransactionObserver: AnyObject { /* ... */ }
```

<table><thead><tr><th width="380">Method</th><th width="110">Required?</th><th>Called when</th></tr></thead><tbody><tr><td><code>onsidePaymentQueue(_:updatedTransactions:)</code></td><td>Yes</td><td>Transactions are added or change state.</td></tr><tr><td><code>onsidePaymentQueue(_:removedTransactions:)</code></td><td>No</td><td>Transactions are removed (after finishing).</td></tr><tr><td><code>onsidePaymentQueueRestoreCompletedTransactionsFinished(_:)</code></td><td>No</td><td>A restore finished successfully.</td></tr><tr><td><code>onsidePaymentQueue(_:restoreCompletedTransactionsFailedWithError:)</code></td><td>No</td><td>A restore failed.</td></tr><tr><td><code>onsidePaymentQueueDidChangeStorefront(_:)</code></td><td>No</td><td>The storefront changed (login/logout/region).</td></tr></tbody></table>

Only `onsidePaymentQueue(_:updatedTransactions:)` is required; the others have default empty implementations. Observers are held **weakly** — keep a strong reference and `remove(observer:)` when done. See [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention.md).

## When the storefront changes

If the user logs in/out or their region changes, the storefront changes: the queue resets, notifies observers via `onsidePaymentQueueDidChangeStorefront(_:)`, and re-validates pending work. A queued purchase that would now run in a different storefront is gated by the delegate so you can confirm or cancel it. See [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes.md).

## Next

* [Making a Purchase](/sdk/purchasing/making-a-purchase.md)
* [Restoring Purchases](/sdk/purchasing/restoring-purchases.md)
* [Models](/sdk/reference/models.md) · [Error Reference](/sdk/reference/errors.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.onside.io/sdk/core-concepts/payment-queue.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
