---
title: "Features & Entitlements"
description: "Check feature access, observe it in SwiftUI, and consume quota or credits."
search:
  tags: ["sdk", "reference", "ios"]
  keywords: ["Swift", "StoreKit", "Xcode", "hasFeature", "useFeatureAndWait", "consumeFeature", "operationId"]
---

Use feature access to determine whether a customer can use a capability in your app. Pass the **Feature key** from Nuxie to the SDK; [products and entitlements](/docs/monetization/features-and-entitlements) determine who receives access.

## Check access

```swift
// nuxie-doc-fragment: basic-feature-access harness=async-body
let access = try await NuxieSDK.shared.hasFeature("premium-content")
if access.allowed {
    showPremiumContent()
} else {
    showUpgradePrompt()
}
```

`hasFeature()` uses fresh cached access and requests the server when necessary. A successful query can return `allowed: false`; a failed query throws.

For a metered feature, pass the required amount and optional entity:

```swift
// nuxie-doc-fragment: metered-feature-access harness=async-body
let access = try await NuxieSDK.shared.hasFeature(
    "ai-generations",
    requiredBalance: 5,
    entityId: "project-456"
)

if access.allowed {
    // This is presentation state only. Consume usage before the side effect.
    showEnabledGenerateButton()
}
```

For shared credits, use the credit feature's own key and the action's cost in credit units.

### Access result

`FeatureAccess` contains `allowed`, `unlimited`, optional `balance`, and `type` (`.boolean`, `.metered`, or `.creditSystem`). `hasAccess` is an alias for `allowed`; `hasBalance` is true for unlimited access or a positive balance.

`requiredBalance` defaults to `1`. `entityId` selects an allowance assigned to an entity such as a project. A check can enable an action in the UI, but it does not reserve the units for that action.

### Cache policy

```swift
// nuxie-doc-fragment: feature-access-policies harness=async-body
let cachedFirst = try await NuxieSDK.shared.hasFeature(
    "premium-content",
    policy: .cacheFirst
)

let authoritative = try await NuxieSDK.shared.hasFeature(
    "premium-content",
    policy: .remote
)
```

`.cacheFirst` is the default. `.remote` always queries the server. Neither policy deducts usage.

## Reactive SwiftUI access

`NuxieSDK.shared.features` updates when the customer's access changes:

```swift
// nuxie-doc-snippet: observable-feature-view
import SwiftUI
import Nuxie

struct PremiumView: View {
    @ObservedObject private var features = NuxieSDK.shared.features

    var body: some View {
        if features.state == .unknown {
            ProgressView()
        } else if features.isAllowed("premium-content") {
            PremiumContent()
        } else {
            UpgradePrompt()
        }
    }
}
```

| Member | Description |
| --- | --- |
| `state` | `.unknown` while loading, `.reconciling` while a purchase is being synchronized, or `.ready`. |
| `all` | Cached access indexed by Feature key. |
| `snapshot` | Access and state from the same update, with `identityGeneration` and `revision`. |
| `isAllowed(_:)` | Whether the feature allows access. |
| `hasBalance(_:)` | Whether access is unlimited or has a positive balance. |
| `balance(_:)` | The metered balance, when available. |
| `feature(_:)` | The full `FeatureAccess` value, when available. |

An empty `.ready` snapshot is valid. Treat `.unknown` as loading rather than denied access.

<span id="server-authoritative-quotas-and-credits" />

## Consume usage

Call `useFeatureAndWait()` before performing an action that deducts quota or credits:

```swift
// nuxie-doc-fragment: consume-feature-and-wait harness=async-body
let result = try await NuxieSDK.shared.useFeatureAndWait(
    "ai-generations",
    amount: 1,
    entityId: "project-456"
)

guard result.success else { return }

let remaining = result.authoritativeAccess?.balance
    ?? result.usage?.remaining
proceedWithGeneration(remaining: remaining)
```

Proceed only when `success` is true. The result includes the remaining balance when available.

After a native App Store purchase without a purchase delegate, the SDK can submit the matching signed purchase with the first usage request so Nuxie grants and deducts the allowance together. This also works in `.observer` mode, where your app finishes the transaction.

With RevenueCat, Superwall, or a custom purchase delegate, Nuxie receives only the checkout outcome. It can complete the Journey, but it does not update Features or balances. Keep using your provider's access checks. Provider Connectors feed Automations and server-side Journeys, without granting or maintaining Feature access. Support for signed transactions from delegated checkout and Feature access updates through Connector webhooks is planned. See [Purchases](/docs/ios-sdk/purchases#access-checks).

With native StoreKit and no purchase delegate, Boolean and unlimited access can update from a verified local purchase. Quotas and credit balances require server confirmation.

### Usage options

| Parameter | Default | Description |
| --- | --- | --- |
| `amount` | `1` | Units to consume, or the cumulative total when `setUsage` is true. |
| `entityId` | `nil` | Entity whose allowance should be charged. |
| `setUsage` | `false` | Report a cumulative total and charge only its increase. |
| `metadata` | `nil` | Additional values recorded with the usage event. |

Amounts must be positive whole numbers no greater than `9,007,199,254,740,991`. With `setUsage: true`, reporting `20` and then `25` charges 20 units and then five more. A lower total does not refund usage.

`FeatureUsageResult` includes `success`, `featureId`, `amountUsed`, optional `message`, optional `usage`, and optional `authoritativeAccess`. `usage` contains `current`, optional `limit`, and optional `remaining`. Do not treat a thrown error as a confirmed denial.

### Retry an operation

Use `consumeFeature(_:quantity:operationId:entityId:)` when your app needs to retain the same operation ID across requests. It is an async throwing method returning `FeatureConsumptionResult`. `useFeatureAndWait()` does not accept an app-supplied operation ID, so use `consumeFeature()` for work your app may need to retry.

Save an operation ID with the action before calling the SDK. Retry for the **same customer**, with the same Feature, quantity, entity, and operation ID. If the signed-in customer has changed, do not retry the previous customer's action under the new identity. Nuxie returns the saved decision instead of deducting units again.

Inspect `accepted` before doing the work, and record that the app has applied the accepted operation so a repeated response does not perform the action twice.

| Receipt field | Description |
| --- | --- |
| `customerId`, `featureId` | Customer and feature charged. |
| `operationId`, `quantity` | The submitted action and amount. |
| `accepted`, `code` | Whether consumption was accepted and its result code. |
| `balance`, `unlimited`, `active` | Access after the operation; balance may be absent. |
| `occurredAtMs` | Optional time of the operation in milliseconds. |
| `idempotentReplay` | The result was already recorded for this operation ID. |

An accepted last use can leave a zero balance or `active: false`. Authorize that action from `accepted`. A timeout does not undo a server-side deduction; retry with the original operation ID.

## Fire-and-forget reporting

Use `useFeature()` when the app does not need to wait for the usage result:

```swift
// nuxie-doc-fragment: enqueue-feature-usage harness=sync-body
NuxieSDK.shared.useFeature(
    "analytics-event",
    amount: 1,
    metadata: ["source": "settings"]
)
```

Errors are logged. Use `useFeatureAndWait()` instead when acceptance determines whether the app may perform the action.

## Change notifications

Assign a `NuxieDelegate` to `NuxieSDK.shared.delegate` to receive access changes:

```swift
// nuxie-doc-snippet: feature-access-observer
import Nuxie

@MainActor
final class FeatureObserver: NuxieDelegate {
    func featureAccessDidChange(
        _ featureId: String,
        from oldValue: FeatureAccess?,
        to newValue: FeatureAccess
    ) {
        print("Feature \(featureId) allowed: \(newValue.allowed)")
    }
}
```

Delegate methods run on the main actor. The SDK stores `delegate` weakly, so retain your observer in an app-owned property for as long as it should receive changes.

<span id="next-steps" />

See [Feature gating](/docs/ios-sdk/feature-gating) for an upgrade flow and [Purchases](/docs/ios-sdk/purchases) for checkout integration.
