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

# Coinme Vault SDK for Android Native

## **Introduction**

The Coinme Vault SDK is a Kotlin library for Android that wraps the VGS Collect Android 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 Android counterpart to the `vault-sdk-react-native` SDK, and the public API mirrors the React Native 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 Android version | Android 7.0 (Nougat) — API Level 24 |
| Compile SDK             | 37                                  |
| JVM target              | 17                                  |
| VGS Collect             | 1.11.0 (re-exported as `api`)       |

The SDK ships **no Compose dependency** — VGS fields are Android Views, so "Compose support" is just `AndroidView` interop (see Integration path 3 below).

<br />

***

## **Installation**

**IMPORTANT:** Contact your Coinme integration team to obtain your Cloudsmith entitlement token. The library publishes to Cloudsmith (`coinme/coinme-sdk-mobile`) as `com.coinme:vault-sdk-android`.

### **Step 1: Add the Repository**

In `settings.gradle.kts`:

```kotlin settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://maven.cloudsmith.io/coinme/coinme-sdk-mobile/")
            // Cloudsmith entitlement token, supplied by Coinme.
            credentials(HttpHeaderCredentials::class) {
                name = "Authorization"
                value = "Bearer ${providers.gradleProperty("CLOUDSMITH_ENTITLEMENT_TOKEN").get()}"
            }
            authentication { create<HttpHeaderAuthentication>("header") }
        }
    }
}
```

### **Step 2: Add the Dependency**

In `app/build.gradle.kts`:

```kotlin app/build.grqadle.kts
dependencies {
    implementation("com.coinme:vault-sdk-android:1.0.0")
}
```

Because the SDK declares `api(vgscollect)`, the VGS field-view classes (`VGSCardNumberEditText`, …) are available on your compile classpath without an extra dependency.

<br />

***

## **Configuration**

The account-scoped **vault ID** is baked into the library as a `BuildConfig` field, read from an **environment variable with a** `local.properties` **fallback**. Copy `local.properties.example` to `local.properties` and fill in the values, or set the env vars in CI:

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

The env var names match the React Native SDK's `gen-vault-config`, so one `.env` serves both repos.

**Ingress hostnames and the submit path are not configured here** — they are public, stable values baked into the SDK per environment: 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**

```kotlin
import com.coinme.vaultsdk.CoinmeVault
import com.coinme.vaultsdk.CoinmeVaultEnvironment
import com.coinme.vaultsdk.core.VaultSubmitOptions
import com.coinme.vaultsdk.widget.CoinmeFields

// 1. Create the vault for an environment.
val vault = CoinmeVault.create(context, CoinmeVaultEnvironment.SANDBOX)

// 2. Build pre-named VGS fields and bind them.
val cardNumber = CoinmeFields.cardNumber(context)
val expiry     = CoinmeFields.expiry(context)
val cvc        = CoinmeFields.cvc(context)
val holder     = CoinmeFields.cardHolder(context)
vault.bind(cardNumber, expiry, cvc, holder)   // add the views to your layout too

// 3. Observe form validity (drive your submit button's enabled state).
lifecycleScope.launch {
    vault.formState.collect { state -> submitButton.isEnabled = state.isValid }
}

// 4. Submit. Throws CoinmeVaultException on failure.
lifecycleScope.launch {
    val result = vault.submit(
        VaultSubmitOptions(
            extraData = mapOf("accountId" to "abc", "country" to "US"),
            headers = mapOf("X-Request-Source" to "android"),
        )
    )
    println("status=${result.status} body=${result.body}")
}

// 5. Release VGS resources when done (e.g. onDestroy / onDispose).
vault.dispose()
```

<br />

***

## **Core Concepts**

| Type                     | Role                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------ |
| `CoinmeVault`            | Entry point. Wraps a `VGSCollect` collector, tracks form validity, performs submits. |
| `CoinmeVaultEnvironment` | `SANDBOX` or `LIVE`. Selects the baked-in vault ID / ingress / path.                 |
| `CoinmeFields`           | Factory that builds VGS field views with the correct Coinme field name pre-set.      |
| `VaultFieldNames`        | The dot-notation field-name constants VGS deep-merges into nested JSON.              |
| `VaultSubmitOptions`     | Per-submit `extraData` (non-secure JSON) and `headers`.                              |
| `VaultSubmitResult`      | `status: Int`, `body: String?`, `data: JsonElement?` on success.                     |
| `CoinmeVaultException`   | Sealed error hierarchy thrown by `submit`.                                           |

The collector is exposed as `vault.collector: VGSCollect` — the documented escape hatch for advanced VGS usage not covered by the wrapper. You rarely need it.

<br />

### **Creating a vault**

```kotlin
val vault = CoinmeVault.create(
    context = context,
    environment = CoinmeVaultEnvironment.SANDBOX,
    config = null,   // optional RuntimeVaultConfig; see "Runtime configuration overrides"
)
```

`create` resolves the vault configuration, builds the underlying `VGSCollect`, and registers a single form-state listener. Create **one** `CoinmeVault` per form/screen and `dispose()` it when the screen goes away.

<br />

### **Field names**

Android VGS has no `{{ placeholder }}` templating. Field names are **dot-notation keys** that VGS deep-merges with `setCustomData` into nested submit JSON. They are defined in `VaultFieldNames`:

| Constant      | Value                         | Notes                                          |
| ------------- | ----------------------------- | ---------------------------------------------- |
| `CARD_NUMBER` | `card.cardNumber`             |                                                |
| `EXP_MONTH`   | `card.month`                  | child of the expiry serializer                 |
| `EXP_YEAR`    | `card.year`                   | child of the expiry serializer                 |
| `CVC`         | `card.cvv`                    |                                                |
| `CARD_HOLDER` | `cardHolderName`              |                                                |
| `ADDRESS1`    | `billingAddress.addressLine1` |                                                |
| `ADDRESS2`    | `billingAddress.addressLine2` |                                                |
| `CITY`        | `billingAddress.city`         |                                                |
| `ZIPCODE`     | `billingAddress.zipCode`      |                                                |
| `EXPIRY`      | `card`                        | parent field name for `ExpirationDateEditText` |

Unlike the RN SDK's template allowlist, **Android 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("card.month", "card.year")` so a single input writes two JSON keys. `CoinmeFields.expiry()` configures this for you.

<br />

***

## **Integration Paths**

The same `CoinmeVault` instance supports three ways of constructing fields.

### **1.&#x20;**`CoinmeFields`**&#x20;factory (Kotlin / Java)**

**Recommended for native (View-based and Compose) usage.**

VGS field views are `final` and cannot be subclassed, so the SDK does not ship `CoinmeXField` subclasses. Instead, `CoinmeFields` is a factory that builds a stock VGS view with the correct default field name already applied.

```kotlin
import com.coinme.vaultsdk.widget.CoinmeFields

val cardNumber = CoinmeFields.cardNumber(context)   // VGSCardNumberEditText, name "card.cardNumber"
val expiry     = CoinmeFields.expiry(context)       // ExpirationDateEditText, separate-date serializer
val cvc        = CoinmeFields.cvc(context)          // CardVerificationCodeEditText, name "card.cvv"
val holder     = CoinmeFields.cardHolder(context)   // PersonNameEditText, name "cardHolderName"
val custom     = CoinmeFields.text(context, "myField") // VGSEditText, arbitrary field name

container.addView(cardNumber)
container.addView(expiry)
vault.bind(cardNumber, expiry, cvc, holder)
```

`cardNumber`, `cvc`, and `cardHolder` accept an optional `fieldName` override; `expiry` does not (its serializer is fixed); `text` requires the field name.

### **2. XML layouts**

Declare the VGS view classes directly in your layout and set `app:fieldName` to the matching `VaultFieldNames` value.

```xml
<com.verygoodsecurity.vgscollect.widget.VGSCardNumberEditText
    android:id="@+id/card_number"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:fieldName="card.cardNumber" />

<com.verygoodsecurity.vgscollect.widget.CardVerificationCodeEditText
    android:id="@+id/cvc"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:fieldName="card.cvv" />

<com.verygoodsecurity.vgscollect.widget.PersonNameEditText
    android:id="@+id/card_holder"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:fieldName="cardHolderName" />
```

```xml
val vault = CoinmeVault.create(this, CoinmeVaultEnvironment.SANDBOX)
vault.bind(
	findViewById(R.id.card_number),    
	findViewById(R.id.cvc),   
	findViewById(R.id.card_holder),
)
```

**Expiry in XML:** the separate-date serializer can't be expressed in XML attributes. Either build the expiry field via `CoinmeFields.expiry(context)` in code and add it to your layout, or set the serializer on the `ExpirationDateEditText` after `findViewById`:

```xml
findViewById<ExpirationDateEditText>(R.id.expiry).setSerializer(
    VGSExpDateSeparateSerializer(VaultFieldNames.EXP_MONTH, VaultFieldNames.EXP_YEAR)
)
```

### **3. Jetpack Compose (AndroidView)**

The SDK ships **no Compose artifact**. VGS fields are Android Views, so Compose support is just `AndroidView` interop. Copy these two reference helpers into your app (the canonical copy lives in the demo at `app/.../screens/VaultFields.kt`):

```kotlin
@Composable
fun rememberCoinmeVault(
    environment: CoinmeVaultEnvironment,
    config: RuntimeVaultConfig? = null,
): CoinmeVault {
    val context = LocalContext.current
    val vault = remember(environment, config) {
        CoinmeVault.create(context, environment, config)
    }
    DisposableEffect(vault) { onDispose { vault.dispose() } }
    return vault
}

/** Builds a Coinme widget, binds it to [vault], and renders it via AndroidView. */
@Composable
fun <T : InputFieldView> VaultField(
    vault: CoinmeVault,
    modifier: Modifier = Modifier,
    factory: (Context) -> T,
    configure: (T.() -> Unit)? = null,
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            factory(context).apply {
                configure?.invoke(this)
                vault.bind(this)
            }
        },
    )
}
```

<br />

***

## **Observing Form State**

```kotlin
val formState: StateFlow<VaultFormState>                    // whole-form snapshot
fun fieldState(fieldName: String): Flow<VaultFieldState?>   // one field

data class VaultFormState(
    val fields: Map<String, VaultFieldState> = emptyMap(),
    val isValid: Boolean = false,   // true only when every bound field is valid
)

data class VaultFieldState(
    val fieldName: String,
    val isValid: Boolean,
    val isEmpty: Boolean,
    val isFocused: Boolean,
    val contentLength: Int,
    val cardBrand: String? = null,  // card-number field only
    val last4: String? = null,      // card-number field only
)
```

Binding seeds each field as invalid (RN parity) — a freshly bound form reports `isValid == false` until VGS emits its first state change.

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

<br />

***

## **Submitting**

```kotlin
suspend fun submit(options: VaultSubmitOptions = VaultSubmitOptions()): VaultSubmitResult

data class VaultSubmitOptions(
    val extraData: Map<String, Any> = emptyMap(),  // deep-merged into the submit JSON alongside fields
    val headers: Map<String, String> = emptyMap(), // 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 `CoinmeVaultException.Validation`**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. Builds a `POST` to the configured submit path with `extraData` as custom data, then calls VGS **synchronously** on `Dispatchers.IO`.
4. Maps the response — including the **CAAS envelope** case where VGS returns HTTP 200 with an `errorResponse` payload (thrown as `CoinmeVaultException.Caas`, not returned as success).

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

<br />

### **Callback variant (Java / non-coroutine)**

```kotlin
vault.submit(VaultSubmitOptions()) { result: Result<VaultSubmitResult> ->
    result.onSuccess { /* ... */ }.onFailure { /* ... */ }
}
```

### **Payment-method convenience**

```kotlin
import com.coinme.vaultsdk.models.PaymentMethodInput
import com.coinme.vaultsdk.models.PaymentProcessAssociation
import com.coinme.vaultsdk.core.submitPaymentMethod

val result = vault.submitPaymentMethod(
    PaymentMethodInput(
        accountId = "abc",
        providerId = "prov-1",
        webSessionId = "sess-1",
        paymentProcessAssociation = PaymentProcessAssociation.BUY,
        // optional billingAddress* fields default to ""/omitted; country defaults to "US"
    )
)
```

It builds the CAAS `extraData` shape (`accountId`, `stagingProviderId`, `webSessionId`, `paymentProcessAssociation`, `billingAddress`) and delegates to `submit`.

<br />

***

## **Error Handling**

`submit` throws `CoinmeVaultException`, a sealed hierarchy:

```kotlin
try {
    vault.submit(options)
} catch (e: CoinmeVaultException.Validation) {
    e.fields.forEach { (name, reasons) -> showFieldError(name, reasons) }
} catch (e: CoinmeVaultException.Caas) {
    when (e.submitCardError) {
        SubmitCardError.CARD_IN_USE_BY_ANOTHER_USER -> showCardInUse()
        SubmitCardError.MAX_NUMBER_OF_CARDS_REACHED -> showTooManyCards()
        else -> showGenericError()
    }
} catch (e: CoinmeVaultException.Server) {
    showServerError(e.status)
} catch (e: CoinmeVaultException) {
    showGenericError()
}
```

| Subclass           | Meaning                                       | Key fields                                                                             |
| ------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |
| `Validation`       | Form invalid; thrown before any network call. | `fields: Map<String, List<String>>` (`"required"` / `"invalid"` per field)             |
| `Network`          | Connectivity / timeout.                       | `cause`                                                                                |
| `Server`           | HTTP 4xx/5xx from the vault.                  | `status: Int`, `body: String?`                                                         |
| `Caas`             | HTTP 200 with a CAAS error envelope.          | `status: Int`, `errorResponse: CaasErrorResponse`, `submitCardError: SubmitCardError?` |
| `Unknown`          | Anything else.                                | `cause`                                                                                |
| `SubmitInProgress` | A submit is already in flight.                | —                                                                                      |

`SubmitCardError` (in `com.coinme.vaultsdk.enums`) maps known CAAS error codes to enum constants and is ported from the Coinme app to ease migration. `e.submitCardError` resolves the first known error code to a `SubmitCardError` (or `null` if the code is unrecognized); the raw code remains available via `e.errorResponse.firstKnownError()` (a `CaasErrorData` with `errorCode`, `message`).

**Exception messages never contain field values (PII).** Log them freely.

<br />

***

## **Runtime Configuration Overrides**

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

```kotlin
val 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"),
)
val vault = CoinmeVault.create(context, CoinmeVaultEnvironment.SANDBOX, runtime)
```

When a `RuntimeVaultConfig` is supplied, both environments' `ingress` and `path` must be non-blank (an `IllegalArgumentException` is thrown otherwise). When it is `null`, the baked-in `BuildConfig` values are used.

<br />

***

## **Lifecycle**

Call `vault.dispose()` when the form is gone (Activity/Fragment `onDestroy`, or Compose `onDispose` via `rememberCoinmeVault`). It cancels the internal coroutine scope and calls `collector.onDestroy()` to release VGS resources. After disposal the instance must not be reused — create a new one.

<br />

***

## **Java Interop**

`CoinmeVault.create`, `CoinmeFields.*`, and the callback-based `submit` are annotated `@JvmStatic` / `@JvmOverloads`, so they read naturally from Java:

```kotlin
CoinmeVault vault = CoinmeVault.create(context, CoinmeVaultEnvironment.SANDBOX);
VGSCardNumberEditText cardNumber = CoinmeFields.cardNumber(context);
vault.bind(cardNumber);

vault.submit(new VaultSubmitOptions(), result -> {
    if (result.isSuccess()) { /* result.getOrNull() */ }
    return Unit.INSTANCE;
});
```

For Flow-based state observation from Java, collect via the standard `kotlinx-coroutines` Java interop or expose a wrapper on the Kotlin side.