# Structured Ingots API: SparkVault API Reference

> Key/value encrypted storage with atomic operations. Store up to 10,000 keys per ingot with server-side encryption and values returned as ephemeral Sparks.

Canonical: https://sparkvault.com/api/docs/products/structured-ingots/ · OpenAPI: https://sparkvault.com/openapi.yaml

## Overview

Structured Ingots provide encrypted key/value storage within your [Vaults](/api/docs/vaults/). Unlike standard ingots (files), structured ingots store named fields that can be individually retrieved without downloading the entire ingot.

10K

Keys per Ingot

1MB

Max Value Size

AES-256

Encryption

Atomic

Set/Delete Ops

### Use Cases

-   **PII Storage**: SSN, phone numbers, addresses per customer
-   **Credentials**: API keys, passwords, secrets with field-level access
-   **User Preferences**: Encrypted user settings with individual key access
-   **Tokenized Data**: Store sensitive data, retrieve as ephemeral Sparks

> **Values Returned as Sparks**
>
> For security, values are never returned inline. When you request keys, you receive Spark IDs that can be read once via the [Sparks API](/api/docs/sparks/).

## Authentication

All Structured Ingots endpoints require account authentication (a JWT Bearer token _or_ an API key) plus a Vault Access Token (VAT) obtained by unsealing the vault.

#### Headers

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Optional | JWT Bearer token (`Authorization: Bearer <token>`). Provide this or `X-API-Key`. |
| `X-API-Key` | string | Optional | Your SparkVault API key. Provide this or `Authorization`. |
| `X-Vault-Access-Token` | string | Required | VAT from unsealing the vault. Missing header → `400 VALIDATION_ERROR`; unknown or malformed VAT → `401 AUTHENTICATION_ERROR`; expired VAT → `403 FORBIDDEN`. |

## API Endpoints

All JSON responses use the `{"data": ...}` envelope. Successful requests return `201 Created` for POST, `200 OK` for GET and PATCH, and `204 No Content` for DELETE.

### Create Structured Ingot

#### `POST /v1/products/structured-ingots/{vault_id}`

Create a new structured ingot with initial key/value data.

#### Request Body

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Required | Name for the ingot (max 255 characters). A create with a name that already exists in the vault overwrites the existing structured ingot in place: it resets `version` to 1 and replaces every stored key, so the prior contents and keys are destroyed. Two structured ingots in one vault cannot share a name; use a fresh name (or PATCH the existing ingot) unless you intend to replace it. |
| `data` | object | Required | Key/value pairs to store (max 10,000 keys) |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `ingot_id` | string | Unique ingot identifier (ing\_...) |
| `name` | string | Ingot name |
| `version` | integer | Version number (starts at 1) |
| `key_count` | integer | Number of keys stored |
| `created_at` | integer | Creation timestamp |

#### Example

Request

```bash
curl -X POST https://api.sparkvault.com/v1/products/structured-ingots/vlt_abc123 \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: YOUR_VAT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "customer_12345",
    "data": {
      "ssn": "123-45-6789",
      "phone": "+1-555-123-4567",
      "dob": "1990-01-15"
    }
  }'
```

Response

```json
{
  "data": {
    "ingot_id": "ing_xyz789...",
    "name": "customer_12345",
    "version": 1,
    "key_count": 3,
    "created_at": 1702000000
  }
}
```

### List Structured Ingots

#### `GET /v1/products/structured-ingots/{vault_id}`

List structured ingots in a vault as a single, non-paginated page.

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | Optional | Maximum number of ingots to fetch before filtering (1-500). This bounds a single fetch across _all_ ingot types in the vault; only structured ingots are then returned, so `count` can be smaller than the true number of structured ingots (even zero) when the vault also holds many file ingots. There is no pagination cursor, so raise the limit if you need more coverage.Default: `100` |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `ingots` | array | Array of structured ingot summaries |
| `ingots[].ingot_id` | string | Ingot identifier |
| `ingots[].name` | string | Ingot name |
| `ingots[].version` | integer|null | Always null in list responses. Versions are only decoded on single-ingot GET |
| `ingots[].key_count` | integer|null | Always null in list responses. Key counts are only decoded on single-ingot GET |
| `ingots[].created_at` | integer | Creation timestamp |
| `ingots[].updated_at` | integer|null | Last update timestamp |
| `count` | integer | Number of ingots returned |

### Get Ingot Metadata

#### `GET /v1/products/structured-ingots/{vault_id}/{ingot_id}`

Get metadata and list of keys (without values).

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `ingot_id` | string | Ingot identifier |
| `name` | string | Ingot name |
| `version` | integer | Current version |
| `key_count` | integer | Number of keys |
| `keys` | array | List of all key names |
| `created_at` | integer | Creation timestamp |
| `updated_at` | integer | Last update timestamp |

#### Example

Request

```bash
curl https://api.sparkvault.com/v1/products/structured-ingots/vlt_abc123/ing_xyz789 \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: YOUR_VAT"
```

Response

```json
{
  "data": {
    "ingot_id": "ing_xyz789...",
    "name": "customer_12345",
    "version": 3,
    "key_count": 5,
    "keys": ["ssn", "phone", "dob", "email", "address"],
    "created_at": 1702000000,
    "updated_at": 1702100000
  }
}
```

### Get Values as Sparks

#### `GET /v1/products/structured-ingots/{vault_id}/{ingot_id}?keys=k1,k2&ttl=900`

Retrieve specific values. Values are returned as Spark IDs for secure, auditable access.

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `keys` | string | Required | Comma-separated list of keys to retrieve (max 100 keys per request) |
| `ttl` | integer | Optional | Spark TTL in seconds (min: 60, max: 86400)Default: `900` |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `ingot_id` | string | Ingot identifier |
| `values` | object | Map of key to Spark info |
| `values[key].spark_id` | string | Spark ID containing the value |
| `values[key].expires_at` | integer | When the Spark expires |
| `missing_keys` | array | Keys that were requested but not found |

#### Example

Request

```bash
curl "https://api.sparkvault.com/v1/products/structured-ingots/vlt_abc123/ing_xyz789?keys=ssn,phone&ttl=300" \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: YOUR_VAT"
```

Response

```json
{
  "data": {
    "ingot_id": "ing_xyz789...",
    "values": {
      "ssn": {
        "spark_id": "spk_abc123...",
        "expires_at": 1702000300
      },
      "phone": {
        "spark_id": "spk_def456...",
        "expires_at": 1702000300
      }
    },
    "missing_keys": []
  }
}
```

> **Reading Spark Values**
>
> Use the returned Spark IDs with the [Sparks API](/api/docs/sparks/) to read the actual values:
>
> ```
> GET /v1/sparks/spk_abc123
> ```

### Update Keys (PATCH)

#### `PATCH /v1/products/structured-ingots/{vault_id}/{ingot_id}`

Atomically set or delete keys. Operations are applied server-side.

#### Request Body

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `set` | object | Optional | Keys to add or update (key: value pairs) |
| `delete` | array | Optional | Key names to delete |

> **At Least One Operation, Optimistic Concurrency**
>
> A PATCH with neither `set` nor `delete` returns `400 VALIDATION_ERROR` ("At least one set or delete operation required"). Concurrent PATCHes to the same ingot are serialized via optimistic locking: the losing request receives `412 PRECONDITION_FAILED`. Re-read the ingot and retry.

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `ingot_id` | string | Ingot identifier |
| `version` | integer | New version number |
| `key_count` | integer | Updated key count |
| `updated_at` | integer | Update timestamp |

#### Example

Request

```bash
curl -X PATCH https://api.sparkvault.com/v1/products/structured-ingots/vlt_abc123/ing_xyz789 \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: YOUR_VAT" \
  -H "Content-Type: application/json" \
  -d '{
    "set": {
      "email": "updated@example.com",
      "address": "123 New Street"
    },
    "delete": ["old_field"]
  }'
```

Response

```json
{
  "data": {
    "ingot_id": "ing_xyz789...",
    "version": 4,
    "key_count": 6,
    "updated_at": 1702100500
  }
}
```

### Delete Ingot

#### `DELETE /v1/products/structured-ingots/{vault_id}/{ingot_id}`

Permanently delete a structured ingot and all its data.

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `204` | status | No Content - ingot deleted successfully |

## Name Addressing

Structured ingots can be addressed by name using a colon prefix instead of the ingot ID. This allows deterministic addressing based on your own identifiers.

| Path | Addressing |
| --- | --- |
| `/structured-ingots/vlt_abc/ing_xyz789` | By ingot ID |
| `/structured-ingots/vlt_abc/:customer_12345` | By name (colon prefix) |

> **Idempotent Lookups**
>
> Name addressing allows you to use your own identifiers (customer IDs, user IDs) without needing to store SparkVault ingot IDs in your database. Because a create reuses an existing same-named ingot, two structured ingots in a vault never share a name, so `:name` addressing is unambiguous for them. A `409 CONFLICT` arises only when a same-named ingot also exists in a different folder (for example a non-structured file ingot), in which case use the ingot ID instead.

## Complete Example

```javascript
// Create a structured ingot for a customer
const createResponse = await fetch(
  `https://api.sparkvault.com/v1/products/structured-ingots/${vaultId}`,
  {
    method: 'POST',
    headers: {
      'X-API-Key': apiKey,
      'X-Vault-Access-Token': vat,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: 'customer_12345',
      data: {
        ssn: '123-45-6789',
        phone: '+1-555-123-4567',
        dob: '1990-01-15'
      }
    })
  }
);

const ingot = (await createResponse.json()).data;
console.log('Created:', ingot.ingot_id);

// Later: Retrieve SSN as a Spark (by name)
const readResponse = await fetch(
  `https://api.sparkvault.com/v1/products/structured-ingots/${vaultId}/:customer_12345?keys=ssn`,
  {
    headers: {
      'X-API-Key': apiKey,
      'X-Vault-Access-Token': vat
    }
  }
);

const { values } = (await readResponse.json()).data;
const ssnSparkId = values.ssn.spark_id;

// Read the Spark to get the actual SSN value
const sparkResponse = await fetch(
  `https://api.sparkvault.com/v1/sparks/${ssnSparkId}`,
  {
    headers: { 'X-API-Key': apiKey }
  }
);

const ssn = (await sparkResponse.json()).data.payload;
console.log('SSN:', ssn);

// Update with new fields
await fetch(
  `https://api.sparkvault.com/v1/products/structured-ingots/${vaultId}/:customer_12345`,
  {
    method: 'PATCH',
    headers: {
      'X-API-Key': apiKey,
      'X-Vault-Access-Token': vat,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      set: { email: 'new@example.com' },
      delete: ['old_field']
    })
  }
);
```

## Constraints

| Limit | Value |
| --- | --- |
| Max keys per ingot | 10,000 |
| Max key name length | 256 characters |
| Max value size (storage) | 1 MB |
| Max wrapped value size (Spark retrieval) | 256,000 bytes |
| Max keys per retrieval request | 100 |
| Max ingot name length | 255 characters |

> **Retrieval Size Cap**
>
> The 1 MB limit applies to _storing_ a value. Retrieval returns values wrapped in Sparks, whose payloads are capped at 256,000 bytes. A value whose base64/JSON-wrapped size exceeds that cap stores fine but fails retrieval with `400 VALIDATION_ERROR`. Keep individual values under ~256 KB wrapped if you need to retrieve them via the API.

## Error Reference

#### Error Responses

| Status | Code | Description |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Invalid request parameters, missing X-Vault-Access-Token header, no set/delete operation on PATCH, or the operation would exceed the 10,000-key limit |
| 401 | `AUTHENTICATION_ERROR` | Missing or invalid credentials, or the Vault Access Token is invalid |
| 403 | `FORBIDDEN` | Expired VAT, VAT issued for a different vault or account, or the ingot is not managed by the Structured Ingots product |
| 404 | `NOT_FOUND` | Vault or ingot not found |
| 409 | `CONFLICT` | Ambiguous :name reference: a same-named ingot also exists in another folder (for example a non-structured file ingot). Use the ingot ID instead |
| 412 | `PRECONDITION_FAILED` | Ingot changed while preparing the update. Re-read and retry the PATCH |
| 429 | `RATE_LIMIT_EXCEEDED` | Rate limit exceeded (300 operations per minute per account) |
