# Mobile SDK: SparkVault API Reference

> Native React Native and Expo SDK for SparkVault Identity, vaults, folders, and ingot transfer.

Canonical: https://sparkvault.com/api/docs/sdk-mobile/ · OpenAPI: https://sparkvault.com/openapi.yaml

## What are you trying to do?

[

### Add login to my React Native app

Passwordless authentication with passkeys and OTP via a native Identity dialog.

](#use-case-login)[

### Upload and download encrypted files

Resumable file transfer scaled for any size with a single canonical path.

](#use-case-transfer)[

### Manage encrypted vaults

CRUD operations, unseal/seal, sharing config, and access logs.

](#use-case-vault)

## Installation

```bash
npm install @sparkvault/sdk-mobile
```

The SDK is host-agnostic by design: you inject platform adapters for HTTP transport, secure token storage, file IO, and (optionally) the native Identity dialog. See [Expo setup](#expo-setup) below for a complete Expo configuration.

> **Native Identity dialog ships as a subpath**
>
> Only consumers that render the `<SparkVaultIdentityDialog />` component need React Native at runtime. Other consumers can import from `@sparkvault/sdk-mobile` directly without pulling React in.

## Modules

The mobile SDK mirrors the web SDK's module model for React Native apps and ships a native Identity dialog driven by the same Identity Product configuration as the web popup.

| Module | Purpose |
| --- | --- |
| `SparkVaultIdentityDialog` | Native Identity popup driven by Identity Product config, branding, and enabled methods |
| `client.products.identity` | Identity Product config, OTP/passkey login, JWKS/JWT verification, and second-factor submission |
| `client.account` | First-party Core session: Identity-token exchange, signup completion, and profile/account |
| `client.vaults` | Vault CRUD, unseal/seal, sharing, access-log retention, and upload portal/widget settings |
| `client.folders` | Folder CRUD, breadcrumbs, folder moves, and ingot moves |
| `client.ingots` | List/search/get, upload/replace/rename/delete, sharing, access logs, downloads |
| `client.health` | Root API availability checks |
| `client.sparks` | SparkSync ephemeral secrets: list/get/create/delete and SparkLink sharing (`share`/`getShare`/`unshare`) |
| `client.entropy` | HSM-backed entropy generation |
| `client.notify` | Metadata-only notifications inbox (`getInbox`, `getUnreadCount`, `markState`) plus Expo device push registration (`registerDevice`, `unregisterDevice`) |
| `client.billing` | Subscription and usage, Stripe customer portal, and native in-app purchase validation |
| `client.users` | Account member management: list, invite, remove, resend invite |

The client also exposes `client.onAuthEvent(listener)`: subscribe to auth/session events (`'logout'`, `'token_refreshed'`) to react to token refresh failures (e.g. sign the user out). It returns an unsubscribe function.

## Expo Setup

Pass the platform adapters the SDK needs into `createSparkVaultMobileClient`. The SDK validates each adapter and fails fast at construction if a required one is missing.

```typescript
import * as FileSystem from 'expo-file-system/legacy';
import { createSparkVaultMobileClient } from '@sparkvault/sdk-mobile';

const client = createSparkVaultMobileClient({
  tokenStorage,                                   // your SecureStore-backed adapter
  identityAccountId: 'acc_YOUR_ACCOUNT_ID',
  userAgent: 'YourApp/1.0.0 (Mobile)',
  fetch: xhrFetch,                                // your fetch wrapper

  fileReader: {
    readAsBase64: (fileUri, { position, length }) =>
      FileSystem.readAsStringAsync(fileUri, {
        encoding: 'base64',
        position,
        length,
      }),
  },

  fileDownloader: {
    download: async (url, fileUri, options) => {
      const resumable = FileSystem.createDownloadResumable(
        url,
        fileUri,
        {},
        progress => options?.onProgress?.({
          bytesWritten: progress.totalBytesWritten,
          bytesTotal: progress.totalBytesExpectedToWrite || undefined,
        })
      );
      const result = await resumable.downloadAsync();
      return result ? { uri: result.uri, status: result.status, headers: result.headers } : null;
    },
    getInfo: async (fileUri) => {
      const info = await FileSystem.getInfoAsync(fileUri);
      return { exists: info.exists, sizeBytes: info.exists ? info.size : undefined };
    },
    delete: (fileUri) => FileSystem.deleteAsync(fileUri, { idempotent: true }),
  },
});
```

> **Adapter contract**
>
> -   `tokenStorage` and `identityAccountId` are required: construction throws if missing (`identityAccountId` must start with `acc_`).
> -   `fetch` falls back to `globalThis.fetch`.
> -   `fileReader` is required for `ingots.uploadFromUri`.
> -   `fileDownloader` is required for `ingots.downloadToFile`.
> -   `logger` is optional: defaults to a no-op logger.
> -   `passkeyProvider` is optional and passed to the Identity dialog, not the client. When absent, the passkey method is hidden from the dialog.
> -   Production downloader adapters must honor `abortSignal` + `timeoutMs`, and throw on transport failure rather than writing an error response as a successful file.

Optional config fields and their defaults:

| Field | Default |
| --- | --- |
| `apiBaseUrl` | `https://api.sparkvault.com/v1` |
| `identityBaseUrl` | `https://auth.sparkvault.com` |
| `preloadIdentityConfig` | `true`: prefetches the Identity Product config at construction |
| `timeoutMs` | `30000`: general request timeout |
| `fileTransferTimeoutMs` | `300000`: file download timeout |
| `tusPostTimeoutMs` | `30000`: tus upload-creation timeout |
| `tusChunkTimeoutMs` | `120000`: per-chunk upload timeout |
| `tusChunkSizeBytes` | `5242880` (5 MiB) |
| `allowedDownloadHostPatterns` | SparkVault platform domains plus `*.amazonaws.com` / `*.cloudfront.net`: signed download URLs are validated against this allowlist |

## Use Case: Native Identity Login

Render `<SparkVaultIdentityDialog />` to handle passkey or OTP verification. The dialog applies your Identity Product branding/theme and returns the raw signed JWT plus JWKS for local verification.

> **Identity Product Endpoint**
>
> The mobile SDK default Identity base URL is `https://auth.sparkvault.com`. JWT verification enforces issuer `https://auth.sparkvault.com/{account_id}` and audience `{account_id}`.

```tsx
import * as Passkey from 'react-native-passkeys';
import {
  SparkVaultIdentityDialog,
  type MobilePasskeyProvider,
} from '@sparkvault/sdk-mobile/identity-dialog';

const passkeyProvider: MobilePasskeyProvider = {
  isSupported: () => Passkey.isSupported(),
  authenticate: async (options) => {
    const credential = await Passkey.get({
      challenge: options.challenge,
      timeout: options.timeout,
      rpId: options.rpId,
      userVerification: options.userVerification,
    });
    return credential
      ? {
          id: credential.id,
          rawId: credential.rawId,
          type: 'public-key',
          response: {
            clientDataJSON: credential.response.clientDataJSON,
            authenticatorData: credential.response.authenticatorData,
            signature: credential.response.signature,
            userHandle: credential.response.userHandle,
          },
        }
      : null;
  },
};

<SparkVaultIdentityDialog
  client={client}
  visible={visible}
  passkeyProvider={passkeyProvider}
  onCancel={() => setVisible(false)}
  onSuccess={async (result) => {
    await client.products.identity.verifyIdentityToken(result.token, { jwks: result.jwks });
    const session = await client.account.exchangeIdentityToken(result.token);

    if ('needs_signup_info' in session) {
      await client.account.completeSignup(session.signup_token, {
        organization_name: 'Acme',
      });
    }
  }}
/>
```

The dialog fetches `/config`, derives email-only, phone-only, or email-or-phone input from enabled delivery methods, applies branding/theme, and offers enabled mobile-supported methods (`passkey`, `otp_email`, `otp_sms`, `otp_voice`). Passkey authentication is discoverable and routes by the SVID in the WebAuthn user handle. Your provider must return `response.userHandle` from the assertion, or the Identity server cannot resolve the account and the login fails. It returns the raw Identity JWT and JWKS metadata so your app can verify before continuing.

Optional dialog props: `initialIdentity?: string` and `initialIdentityType?: 'email' | 'phone'` pre-fill the identity input, and `onError?(error)` surfaces dialog errors to your app.

> **Security Critical**
>
> **Always verify the JWT before trusting it.** Call `client.products.identity.verifyIdentityToken(token, { jwks })` before `account.exchangeIdentityToken`. The SDK pins the signature algorithm to `EdDSA`, verifies the signature with cached JWKS, and enforces `iss`/`aud`/`exp`/`nbf`/`iat` claims.

> **Third-Party App Sessions**
>
> `exchangeIdentityToken()` creates SparkVault Core mobile sessions. Apps using Identity as their own login provider should verify the Identity JWT on their backend, key product records by `svid`, and either issue their own native app session or use OIDC authorization code + PKCE. Native refresh tokens belong in OS secure storage; the browser BFF cookie model applies only to web clients.

## Use Case: Upload, Download, Replace

All transfer flows go through a single canonical path. Upload uses Forge with the tus v1.0.0 resumable protocol; download streams to a file URI via your `fileDownloader` adapter with automatic retry on transient failures. There are no size-based forks: every transfer scales from 1 byte to 5 TB.

```typescript
const session = await client.vaults.unseal(vaultId, { vmk });

// Upload from a local file URI
await client.ingots.uploadFromUri({
  vaultId,
  vat: session.vat,
  fileUri,
  fileSize,
  name: 'photo.jpg',
  contentType: 'image/jpeg',
  folder: 'Trips/2026',  // optional vault path; missing segments are find-or-created
  onProgress: (uploaded, total) => console.log(uploaded / total),
});

// Replace an existing ingot's data
await client.ingots.replaceFromUri({
  vaultId,
  ingotId,
  vat: session.vat,
  fileUri,
  fileSize,
  name: 'photo.jpg',
  contentType: 'image/jpeg',
});

// Download to a file URI: single OS-streaming path, retries with fresh URLs
const result = await client.ingots.downloadToFile({
  vaultId,
  ingotId,
  vat: session.vat,
  fileUri: `${FileSystem.cacheDirectory}photo.jpg`,
  expectedSizeBytes,
  onProgress: (written, total) => console.log(written / total),
});
```

The lower-level primitives behind these flows are also exported: `ingots.createUpload(options)` initializes the ingot record and returns the Forge resumable-upload URL with its upload-session token (the canonical first step of every upload), and `ingots.createDownloadLink(options)` mints a fresh short-lived signed download URL validated against the allowed-host policy (the canonical first step of every download).

## Use Case: Vault Management

```typescript
// List, get, create, update, delete
const vaults = await client.vaults.list();
const vault = await client.vaults.get(vaultId);
const created = await client.vaults.create({ name: 'My Vault' });
await client.vaults.update(vaultId, { name: 'Renamed' });
await client.vaults.delete(vaultId);

// Unseal / seal
const session = await client.vaults.unseal(vaultId, { vmk });
await client.vaults.seal(vaultId);

// Sharing
await client.vaults.enableSharing(vaultId, vmk, { all_ingots_public: false });
await client.vaults.disableSharing(vaultId);

// Folders + ingots inside a vault
const folders = await client.folders.list(vaultId, session.vat);
const ingots = await client.ingots.list(vaultId, session.vat);
```

## Health Checks

```typescript
const status = await client.health.check();
// status.online: true when the API returns 2xx
// status.status: 'healthy' | 'unhealthy' | 'unreachable'
// status.httpStatus: HTTP status when a response was received
// status.checkedAt: Unix timestamp
// status.error: network/timeout message when unreachable

const online = await client.health.isOnline();
```

## Errors

All SDK failures throw typed error classes, exported from the package root. Every class extends `SparkVaultMobileError`, which carries `code`, `statusCode`, `details`, and `meta`.

| Class | Thrown when |
| --- | --- |
| `SparkVaultMobileError` | Base class for all SDK errors |
| `SparkVaultValidationError` | Invalid input or config (400) |
| `SparkVaultAuthenticationError` | Authentication required (401) |
| `SparkVaultAuthorizationError` | Forbidden (403) |
| `PlanRequiredError` | 402 `PLAN_REQUIRED` billing gate: the account has no active subscription; route the user into the in-app subscribe flow |
| `QuotaExceededError` | 402 `QUOTA_EXCEEDED` billing gate: a pooled-capacity or seat limit was reached; `resource` is `'storage' | 'bandwidth' | 'seats' | 'identity'` |
| `SparkVaultNetworkError` | Network request failed |
| `SparkVaultTimeoutError` | Request timed out (408) |
| `TusUploadError` | Resumable upload failure: carries `filename`, `fileSize`, and `phase` (`'create' | 'upload' | 'network' | 'timeout' | 'cancelled' | 'stalled' | 'unknown'`) |
| `IngotNotReadyError` | 400 `INGOT_NOT_READY`: a download was attempted while the ingot is still `uploading`/`encrypting`; carries `ingotStatus`. Back off and retry once the ingot becomes active |

The helper `gateErrorFromBody(statusCode, errorBody)` is also exported: it converts a 402 response body into a typed `PlanRequiredError`/`QuotaExceededError` (or `null` for unrecognized gates), so subscribe/add-on UX can key off one set of error types across the JSON and tus paths.

For download-retry logic, branch on the exported type guards `isIngotNotReadyError(error)` and `isRetryableDownloadError(error)` rather than inspecting `code` directly.

## Resources

-   **Source**: `packages/sdk-mobile/`
-   **npm**: [@sparkvault/sdk-mobile](https://www.npmjs.com/package/@sparkvault/sdk-mobile)
-   See also: [JavaScript SDK](/api/docs/sdk-js/) for browser / Node.
