---
updatedAt: 2026-08-06T19:19:41.000Z
agentTools:
  projectIndex: https://docs.coinme.com/llms.txt
---

# Coinme Vault SDK for iOS Native

## **Introduction**

The Coinme Vault SDK for iOS is a Swift SDK that wraps the VGS Collect iOS SDK, pre-binds Coinme's VGS vault configuration, and exposes a small, secure data-collection API. It lets your application securely collect sensitive card and payment-method data (card number, expiry, CVV, cardholder name, billing address) and submit it directly to Coinme's vault without that data ever entering your application process.

It is the iOS counterpart to the Coinme Vault SDK for Android, and the public API mirrors the Android SDK for cross-platform parity.

**Security model:** Secure field values (card number, CVV, …) never leave the VGS field views and never enter your application process. The SDK exposes only validity and non-sensitive metadata (`isValid`, `isEmpty`, `cardBrand`, `last4`, …). Submission goes directly from the VGS views to the vault. **Error messages never contain field values (PII).**

<br />

### **What Does It Collect?**

The SDK collects only the payment data you explicitly bind into its fields, and submits it directly to the Coinme vault. The supported fields are:

* **Card data** — card number, expiry month/year, CVV
* **Cardholder name**
* **Billing address** — address lines, city, ZIP code

Your application never receives the raw field values — it sees only validity state and non-sensitive metadata such as the detected card brand and the last four digits.

<br />

***

## **SDK Requirements**

| Requirement | Value                             |
| ----------- | --------------------------------- |
| Minimum iOS | 15.0                              |
| Swift       | 5.9                               |
| VGS Collect | `~> 1.18` (resolves transitively) |

VGS Collect is resolved as a normal dependency, so the VGS field-view classes (`VGSCardTextField`, …) are available on your compile classpath without an extra dependency.

<br />

***

## **Installation**

**IMPORTANT:** Contact your Coinme integration team to obtain your Cloudsmith **entitlement token**. The library publishes to Cloudsmith (`coinme/coinme-sdk-mobile`) for both Swift Package Manager and CocoaPods.

<br />

### **Swift Package Manager**

One-time registry login (uses your Cloudsmith entitlement token):

```text
swift package-registry set https://swift.cloudsmith.io/coinme/coinme-sdk-mobile/swift package-registry login https://swift.cloudsmith.io/coinme/coinme-sdk-mobile/ \    --username YOUR_USERNAME --password YOUR_ENTITLEMENT_TOKEN
```

Then add the dependency:

```swift Package.swift
// Package.swift
.package(id: "coinme.CoinmeVaultSDK", from: "1.0.0")
```

The SDK pulls VGSCollectSDK via SCM. To resolve it through the registry instead, run:

```text
swift package --replace-scm-with-registry resolve
```

To add the package in Xcode:&#x20;

**File → Add Package Dependencies**, add the registry URL above, then search for `coinme.CoinmeVaultSDK`.

<br />

### **CocoaPods**

```swift Podfile
# Podfile — <ENTITLEMENT_TOKEN> supplied by Coinme
source 'https://dl.cloudsmith.io/<ENTITLEMENT_TOKEN>/coinme/coinme-sdk-mobile/cocoapods/index.git'
source 'https://cdn.cocoapods.org/'

target 'YourApp' do
  use_frameworks!
  pod 'CoinmeVaultSDK', '~> 1.0'   # pulls VGSCollectSDK transitively
end
```

<br />

***

## **Configuration**

The account-scoped **vault ID** is baked into the library per environment at publish time and is never passed by callers. For local development, copy `local.properties.example` to `local.properties` and fill in:

```text
COINME_VAULT_ID_SANDBOX=your_sandbox_vault_id
COINME_VAULT_ID_LIVE=your_live_vault_id
```

**Ingress hostnames and the submit path are not configured here** — they are public, stable values baked into the SDK per environment (`config/VaultDefaults.swift`): sandbox → `secure.payments-stage.coinme.com`, live → `secure.payments.coinme.com`, path → `/caas/v2/paymentmethods`. They can be overridden per-runtime (but never the vault ID) via `RuntimeVaultConfig` — see Runtime configuration overrides below.

<br />

***

## **Quick Start**

```swift
import CoinmeVaultSDK

// 1. Create the vault for an environment.
let vault = try CoinmeVault.create(environment: .sandbox)

// 2. Build pre-named VGS fields against the vault's collector and bind them.
let cardNumber = CoinmeFields.cardNumber(collector: vault.collector)
let expiry     = CoinmeFields.expiry(collector: vault.collector)
let cvc        = CoinmeFields.cvc(collector: vault.collector)
let holder     = CoinmeFields.cardHolder(collector: vault.collector)
vault.bind(cardNumber, expiry, cvc, holder)   // add the views to your view hierarchy too

// 3. Observe form validity (drive your submit button's enabled state).
vault.onFormStateChange = { state in submitButton.isEnabled = state.isValid }

// 4. Submit. Throws CoinmeVaultError on failure.
let result = try await vault.submit(
    VaultSubmitOptions(
        extraData: ["accountId": "abc", "country": "US"],
        headers: ["X-Request-Source": "ios"]
    )
)
print("status=\(result.status) body=\(result.body ?? "")")

// 5. Release VGS resources when done (e.g. on screen teardown).
vault.dispose()
```

`CoinmeVault` is `@MainActor` — create it, build fields, bind, and observe state on the main actor.

<br />

***

## **Core Concepts**

| Type                     | Role                                                                                               |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `CoinmeVault`            | Entry point. Wraps a `VGSCollect` collector, tracks form validity, performs submits. `@MainActor`. |
| `CoinmeVaultEnvironment` | `.sandbox` or `.live`. Selects the baked-in vault ID / ingress / path.                             |
| `CoinmeFields`           | Factory that builds VGS text fields with the correct Coinme field name and VGS type pre-set.       |
| `VaultFieldNames`        | The dot-notation field-name constants VGS deep-merges into nested submit JSON.                     |
| `VaultSubmitOptions`     | Per-submit `extraData` (non-secure JSON) and `headers`.                                            |
| `VaultSubmitResult`      | `status: Int`, `body: String?`, `data: Any?` on success.                                           |
| `CoinmeVaultError`       | Error enum thrown by `submit`.                                                                     |

The collector is exposed as `vault.collector: VGSCollect` — both the input to the `CoinmeFields` factories and the documented escape hatch for advanced VGS usage not covered by the wrapper.

<br />

### **Creating a vault**

```swift
let vault = try CoinmeVault.create(
    environment: .sandbox,
    config: nil   // optional RuntimeVaultConfig; see "Runtime configuration overrides"
)
```

`create()` resolves the vault configuration, builds the underlying `VGSCollect`, and registers a single form-state listener. It throws `CoinmeVaultError.invalidConfiguration` if the vault ID is not baked in or an override is incomplete. Create **one** `CoinmeVault` per form/screen and `dispose()` it when the screen goes away.

<br />

### **Field names**

Field names are **dot-notation keys** that VGS deep-merges into nested submit JSON (`card.cardNumber` → `{"card":{"cardNumber":...}}`). They are defined in `VaultFieldNames`:

| Constant     | Value                         | Notes                                  |
| ------------ | ----------------------------- | -------------------------------------- |
| `cardNumber` | `card.cardNumber`             |                                        |
| `expMonth`   | `card.month`                  | child of the expiry serializer         |
| `expYear`    | `card.year`                   | child of the expiry serializer         |
| `cvc`        | `card.cvv`                    |                                        |
| `cardHolder` | `cardHolderName`              |                                        |
| `address1`   | `billingAddress.addressLine1` |                                        |
| `address2`   | `billingAddress.addressLine2` |                                        |
| `city`       | `billingAddress.city`         |                                        |
| `zipCode`    | `billingAddress.zipCode`      |                                        |
| `expiry`     | `card`                        | parent field name for the expiry field |

**The SDK submits every bound field.** Bind only the fields you intend to send.

The expiry field is the special case: its own field name is `card` and it uses a `VGSExpDateSeparateSerializer(monthFieldName: "card.month", yearFieldName: "card.year")` so a single input writes two JSON keys. The year is forced to 4 digits on output. `CoinmeFields.expiry()` configures all of this for you.

<br />

***

## **Building Fields**

VGS field views subscribe to their collector the moment their `configuration` is set, so each `CoinmeFields` factory takes the owning vault's collector:

```swift
import CoinmeVaultSDK

let cardNumber = CoinmeFields.cardNumber(collector: vault.collector)  // VGSCardTextField, "card.cardNumber"
let expiry     = CoinmeFields.expiry(collector: vault.collector)      // VGSExpDateTextField, separate-date serializer
let cvc        = CoinmeFields.cvc(collector: vault.collector)         // VGSCVCTextField, "card.cvv"
let holder     = CoinmeFields.cardHolder(collector: vault.collector)  // VGSTextField, "cardHolderName"
let custom     = CoinmeFields.text(collector: vault.collector, fieldName: "billingAddress.city")
```

`cardNumber`, `cvc`, and `cardHolder` accept an optional `fieldName` override; `expiry` does not (its serializer is fixed); `text` requires the field name. Returned views are ordinary `VGSTextField` subclasses — style them, add them to your view hierarchy, and `vault.bind(...)` them.

<br />

### **Rendering VGS fields**

VGS fields are UIKit `UIView`s. In UIKit, add them to your view hierarchy directly. In SwiftUI, wrap each field in a `UIViewRepresentable` (the demo's `VGSFieldView` is the canonical reference):

```swift
struct VGSFieldView: UIViewRepresentable {
    let field: VGSTextField
    func makeUIView(context: Context) -> VGSTextField { field }
    func updateUIView(_ uiView: VGSTextField, context: Context) {}
}
```

Build the fields once (e.g. in your view model) so they aren't recreated on every SwiftUI render, then render each with `VGSFieldView(field: cardNumber)`. See the `Example/` app for a full working form.

<br />

### **Binding fields**

```swift
func bind(_ fields: VGSTextField...)          // required fields
func bindOptional(_ fields: VGSTextField...)  // optional fields
```

`bind` registers each field name with the form-validity tracker. **Registration seeds the field as invalid** — a freshly bound form reports `isValid == false` until VGS emits its first state change. Bind a field once.

VGS emits no initial state, so an untouched field's validity is unknown until the user edits it. Use `bindOptional` for fields that may be left empty (e.g. `billingAddress.addressLine2`): they seed as **valid**, so an untouched form can still reach `isValid == true`. Fields with no configured `fieldName` are skipped by `bind`/`bindOptional` — always build them via `CoinmeFields` first.

<br />

***

## **Observing Form State**

```swift
var formState: VaultFormState                                   // current snapshot
var formStatePublisher: AnyPublisher<VaultFormState, Never>     // Combine stream
var onFormStateChange: ((VaultFormState) -> Void)?             // non-Combine callback
func fieldState(_ fieldName: String) -> VaultFieldState?        // one field

public struct VaultFormState: Equatable {
    public let fields: [String: VaultFieldState]
    public let isValid: Bool   // true only when every bound field is valid
}

public struct VaultFieldState: Equatable {
    public let fieldName: String
    public let isValid: Bool
    public let isEmpty: Bool
    public let isFocused: Bool
    public let contentLength: Int
    public let cardBrand: String?  // card-number field only
    public let last4: String?      // card-number field only
}
```

Typical use — drive the submit button:

```swift
// Callback style:
vault.onFormStateChange = { state in submitButton.isEnabled = state.isValid }

// Combine style:
vault.formStatePublisher
    .map(\.isValid)
    .receive(on: RunLoop.main)
    .assign(to: \.isEnabled, on: submitButton)
    .store(in: &cancellables)

// React to a single field, e.g. show the detected card brand:
let brand = vault.fieldState(VaultFieldNames.cardNumber)?.cardBrand
```

**No secure values are ever present in these snapshots** — only validity and the non-sensitive metadata shown above.

<br />

***

## **Submitting the Data**

```swift
@discardableResult
func submit(_ options: VaultSubmitOptions = VaultSubmitOptions()) async throws -> VaultSubmitResult

public struct VaultSubmitOptions {
    public let extraData: [String: Any]   // deep-merged into the submit JSON alongside fields
    public let headers: [String: String]  // merged with trace headers; caller wins on conflict
}
```

What `submit` does, in order:

1. **Pre-validates** from the tracker; if the form is invalid it throws `CoinmeVaultError.validation(fields:)` **before any network call**.
2. Generates per-request `X-B3-TraceId` / `X-B3-SpanId` and merges your `headers` on top (your headers win on conflict).
3. POSTs to the configured submit path with `extraData` as custom data via VGS.
4. Maps the response — including the **CAAS envelope** case where VGS returns HTTP 200 with a nested `errorResponse` payload (thrown as `CoinmeVaultError.caas`, not returned as success).

**Only one submit may be in flight at a time** — a concurrent call throws `CoinmeVaultError.submitInProgress`.

<br />

### **Completion-handler variant**

```swift
vault.submit(VaultSubmitOptions()) { (result: Result<VaultSubmitResult, Error>) in
    switch result {
    case .success(let r): // ...
    case .failure(let e): // ...
    }
}
```

<br />

### **Payment-method convenience**

A higher-level helper for the CAAS payment-method endpoint:

```swift
let result = try await vault.submitPaymentMethod(
    PaymentMethodInput(
        accountId: "abc",
        providerId: "prov-1",
        webSessionId: "sess-1",
        paymentProcessAssociation: .buy,
        // optional billingAddress* fields default to ""/omitted; country defaults to "US"
        billingAddressFirstName: "Jane",
        billingAddressLastName: "Doe",
        billingAddressState: "WA"
    ),
    headers: ["Authorization": "Bearer \(token)"]
)
```

It builds the CAAS `extraData` shape (`accountId`, `stagingProviderId`, `webSessionId`, `paymentProcessAssociation`, `billingAddress`) and delegates to `submit`. The card fields themselves are supplied securely by the bound VGS fields.

<br />

***

## **Error Handling**

`submit` only ever throws `CoinmeVaultError`

```swift
do {
    try await vault.submit(options)
} catch let error as CoinmeVaultError {
    switch error {
    case .validation(let fields):
        fields.forEach { name, reasons in showFieldError(name, reasons) }
    case .caas:
        switch error.submitCardError {
        case .cardInUseByAnotherUser: showCardInUse()
        case .maxNumberOfCardsReached: showTooManyCards()
        default: showGenericError()
        }
    case .server(let status, _):
        showServerError(status)
    default:
        showGenericError()
    }
}
```

<br />

| Case                           | Meaning                                                   | Associated values                                               |
| ------------------------------ | --------------------------------------------------------- | --------------------------------------------------------------- |
| `.validation(fields:)`         | Form invalid; thrown before any network call.             | `[String: [String]]` — `["required"]` / `["invalid"]` per field |
| `.network(underlying:)`        | Connectivity / transport failure.                         | `Error?`                                                        |
| `.server(status:body:)`        | HTTP 4xx/5xx with no recognizable CAAS envelope.          | `Int`, `String?`                                                |
| `.caas(status:errorResponse:)` | CAAS error envelope (4xx/5xx, or nested in an HTTP 200).  | `Int`, `CaasErrorResponse`                                      |
| `.submitInProgress`            | A submit is already in flight.                            | —                                                               |
| `.invalidConfiguration(_:)`    | Missing/invalid configuration (e.g. an unbaked vault ID). | `String`                                                        |
| `.unknown(underlying:)`        | Anything else.                                            | `Error?`                                                        |

<br />

`error.submitCardError` is a convenience on `CoinmeVaultError` that resolves the first known CAAS error code to a `SubmitCardError` (or `nil` if unrecognized). The raw code remains available via the associated `CaasErrorResponse` (`errorResponse.firstKnownError()?.errorCode`). `SubmitCardError` maps known CAAS card-submit codes to enum constants and is ported from the Coinme app to ease migration.

**Error cases never carry a field value (PCI).** Log them freely.

<br />

***

## **Runtime Configuration Overrides**

The vault ID is **always** baked in at publish time and cannot be overridden. The ingress host and submit path **can** be overridden at runtime by passing a `RuntimeVaultConfig` to `create`:

```swift
let runtime = RuntimeVaultConfig(
    sandbox: EnvironmentOverride(ingress: "tnt-xyz.sandbox.verygoodproxy.com", path: "/caas/v2/paymentmethods"),
    live:    EnvironmentOverride(ingress: "tnt-xyz.live.verygoodproxy.com",    path: "/caas/v2/paymentmethods")
)
let vault = try CoinmeVault.create(environment: .sandbox, config: runtime)
```

When `config` is `nil`, the baked-in defaults are used: a blank ingress means the VGS default host, and the path defaults to `/caas/v2/paymentmethods`.

<br />

***

## **Lifecycle**

Call `vault.dispose()` when the form is gone. It stops state observation and unsubscribes all VGS text fields to release resources. After disposal the instance must not be reused — create a new one.

In SwiftUI, tie disposal to the view's lifetime (e.g. call `dispose()` from `.onDisappear` on the view model that owns the vault).