Overview
Ingots are encrypted data objects stored within Vaults. All ingot operations require two credentials: an API key (or session JWT) to authenticate the caller, plus a valid Vault Access Token (VAT) obtained by unsealing the vault.
Named-field key/value data with atomic updates is a separate product, Structured Ingots, with its own endpoints and semantics. This page covers file ingots only.
Features
| Feature | Description |
|---|---|
| Size | 1 byte to 5 TB per ingot |
| Encryption | AES-256-GCM under the per-vault Vault Access Key (VAK), HKDF-derived from SVMK + AMK + VMK (Triple Zero-Trust). Forge encrypts each 50 MB chunk with the VAK using per-chunk derived nonces |
| Streaming | Server-side encryption/decryption via Forge TUS uploads |
All ingot operations require two credentials: an API key (X-API-Key) or session JWT (Authorization: Bearer) to authenticate the caller, plus the X-Vault-Access-Token header with a valid VAT. See the Vaults API for how to obtain a VAT.
Standard Ingots
Standard ingots are file-based encrypted storage. Upload any file up to 5 TB through the same Forge TUS flow.
{ingot_id} in the endpoints below accepts either an ingot ID (ing_...) or a name reference: a colon followed by the name, e.g. /ingots/:report.pdf. Name lookups are case-insensitive and scoped to your account. Names are unique per folder, not per vault: when the same name exists in multiple folders, a name reference returns 409 CONFLICT with the candidate ingot_id/folder_id pairs. Use the ingot ID instead.
Create Standard Ingot
/v1/vaults/{vault_id}/ingots
Create a new standard ingot and get a Forge URL for uploading. Two-step process: create the ingot record, then upload to Forge with TUS.
Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
X-Vault-Access-Token |
string | Required | Valid VAT from unseal operation |
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Required | File name or identifier |
size_bytes |
integer | Required | Exact file size in bytes (1 byte to 5 TB = 5497558138880 bytes) |
content_type |
string | Optional | MIME typeDefault: application/octet-stream |
original_filename |
string | Optional | Original file name, used for the Content-Disposition header on downloads |
folder_id |
string | Optional | ID of an existing folder to place the ingot in. Mutually exclusive with folder_path |
folder_path |
string | Optional | Slash-delimited folder path (e.g. reports/2026), found or created from the vault root. Mutually exclusive with folder_id |
Creating an ingot with a name that already exists in the same folder upserts: the existing ingot's ID is reused and its content is replaced when the upload completes. An active ingot stays readable until Forge finalizes the new upload.
Response
| Field | Type | Description |
|---|---|---|
ingot_id |
string | Unique ingot identifier (ing_...) |
name |
string | Ingot name |
size_bytes |
integer | Declared size in bytes |
content_type |
string | MIME type |
status |
string | Initial status: uploading |
forge_url |
string | Forge URL containing the upload ISTK |
expires_at |
integer | Upload URL expiration timestamp |
import * as tus from 'tus-js-client';
// Step 1: Create ingot record
const file = document.querySelector('input[type="file"]').files[0];
const createResponse = await fetch(
`https://api.sparkvault.com/v1/vaults/${vaultId}/ingots`,
{
method: 'POST',
headers: {
'X-API-Key': apiKey,
'X-Vault-Access-Token': vat,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: file.name,
size_bytes: file.size,
content_type: file.type
})
}
);
const { ingot_id, forge_url } = (await createResponse.json()).data;
// Step 2: Upload file to Forge with TUS
const forgeUrl = new URL(forge_url);
const istk = forgeUrl.searchParams.get('istk');
const upload = new tus.Upload(file, {
endpoint: `${forgeUrl.origin}/encrypt`,
chunkSize: 50 * 1024 * 1024,
headers: { 'X-ISTK': istk },
retryDelays: [0, 1000, 3000, 5000, 10000]
});
upload.start();
// Status will change: uploading → active
List Ingots
/v1/vaults/{vault_id}/ingots
List all ingots in an unsealed vault.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit |
integer | Optional | Maximum ingots to return (max: 200)Default: 100 |
sort_by |
string | Optional | Sort field: name, size, content_type, created_at, access_count, or bandwidth |
sort_order |
string | Optional | asc or descDefault: asc |
Response
| Field | Type | Description |
|---|---|---|
ingots |
array | Array of ingot objects |
count |
integer | Number of ingots returned |
ingots[].ingot_id |
string | Ingot identifier |
ingots[].name |
string | Ingot name |
ingots[].file_type |
string | Category derived from the file extension (e.g. image, video, document) |
ingots[].size_bytes |
integer | Size in bytes |
ingots[].content_type |
string | MIME type |
ingots[].status |
string | Status: uploading, encrypting, active, or failed |
ingots[].folder_id |
string | Containing folder ID; null for the vault root |
ingots[].managed_by |
string | Product that manages this ingot (e.g. structured-ingots, product-messaging); null for standard ingots. Managed ingots reject direct read/download/update/delete with 403; use the product's endpoints |
ingots[].upload_source |
string | Origin of the upload (uploader's email, or API: {key name} for API-key uploads); null when unknown |
ingots[].created_at |
integer | Creation timestamp |
ingots[].updated_at |
integer | Last-modified timestamp (bumps on every write) |
ingots[].access_count |
integer | Number of downloads |
ingots[].total_bandwidth_bytes |
integer | Total bytes served for this ingot |
Search Ingots
/v1/vaults/{vault_id}/ingots/search
Search and filter ingots within a vault using indexed queries. Filters can be combined.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q |
string | Optional | Name prefix search (case-insensitive). Prefix-only: no substring or fuzzy matching |
file_type |
string | Optional | Filter by category: image, video, audio, document, spreadsheet, presentation, archive, code, design, cad, ebook, font, or other |
min_size |
integer | Optional | Minimum file size in bytes |
max_size |
integer | Optional | Maximum file size in bytes |
limit |
integer | Optional | Maximum results per page (max: 200)Default: 50 |
cursor |
string | Optional | Signed pagination cursor from the previous response's next_cursor |
sort_order |
string | Optional | asc or descDefault: desc |
Response
| Field | Type | Description |
|---|---|---|
ingots |
array | Matching ingot objects (same shape as List Ingots) |
count |
integer | Number of ingots in this page |
has_more |
boolean | Whether more results are available |
next_cursor |
string? | Signed cursor for the next page (present when has_more is true) |
filters |
object | Echo of the applied filters: q, file_type, min_size, max_size |
# Files starting with "Q3"
curl "https://api.sparkvault.com/v1/vaults/$VAULT_ID/ingots/search?q=Q3" \
-H "X-API-Key: sv_live_xxx" \
-H "X-Vault-Access-Token: $VAT"
# Large videos (over 1 GB), newest first
curl "https://api.sparkvault.com/v1/vaults/$VAULT_ID/ingots/search?file_type=video&min_size=1073741824" \
-H "X-API-Key: sv_live_xxx" \
-H "X-Vault-Access-Token: $VAT"
Get Ingot Metadata
/v1/vaults/{vault_id}/ingots/{ingot_id}
Retrieve an ingot's metadata. Use the download endpoint to retrieve decrypted contents.
Response
| Field | Type | Description |
|---|---|---|
ingot_id |
string | Ingot identifier |
name |
string | Ingot name |
size_bytes |
integer | Size in bytes |
content_type |
string | MIME type |
status |
string | Status: uploading, encrypting, active, or failed |
managed_by |
string | Managing product ID; null for standard ingots |
created_at |
integer | Creation timestamp |
updated_at |
integer | Last-modified timestamp. Pass it as expected_updated_at to optimistically lock a subsequent PUT |
access_count |
integer | Number of downloads |
total_bandwidth_bytes |
integer | Total bytes served for this ingot |
Download Ingot
/v1/vaults/{vault_id}/ingots/{ingot_id}/download
Get a time-limited download URL for decrypting an ingot via Forge.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
stream |
boolean | Optional | Mint a reusable, seek-able streaming decrypt URL (many Range requests can be made against one link, e.g. media playback) instead of the single-use defaultDefault: false |
Response
| Field | Type | Description |
|---|---|---|
status |
string | Status (always ready) |
download_url |
string | Signed Forge URL for streaming decryption (includes ISTK) |
ingot_id |
string | Ingot identifier |
name |
string | Original file name |
size_bytes |
integer | File size in bytes |
content_type |
string | MIME type |
original_filename |
string | Original filename for Content-Disposition |
expires_at |
integer | Download URL expiration timestamp |
stream |
boolean | Whether the URL is a reusable streaming URL |
// Step 1: Get download URL
const downloadResponse = await fetch(
`https://api.sparkvault.com/v1/vaults/${vaultId}/ingots/${ingotId}/download`,
{
method: 'POST',
headers: {
'X-API-Key': apiKey,
'X-Vault-Access-Token': vat
}
}
);
const { download_url, name } = (await downloadResponse.json()).data;
// Step 2: Fetch decrypted content from Forge
const fileResponse = await fetch(download_url);
const blob = await fileResponse.blob();
// Trigger browser download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
Replace Ingot Content
/v1/vaults/{vault_id}/ingots/{ingot_id}
Replace an ingot's content. Same two-step flow as create: the response includes a fresh Forge URL for the TUS upload. An active ingot stays readable until Forge finalizes the new upload.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
size_bytes |
integer | Required | Exact size of the replacement content in bytes (1 byte to 5 TB) |
name |
string | Optional | Rename the ingot as part of the replace. Must not collide with another name in the same folder |
content_type |
string | Optional | New MIME type; defaults to the existing content type |
expected_updated_at |
integer | Optional | Optimistic lock: the ingot's updated_at from your last read. If the ingot changed since, the request fails with 412 PRECONDITION_FAILED |
Response
| Field | Type | Description |
|---|---|---|
ingot_id |
string | Ingot identifier (unchanged) |
name |
string | Ingot name (after any rename) |
size_bytes |
integer | Declared replacement size in bytes |
content_type |
string | MIME type |
status |
string | Status: uploading |
forge_url |
string | Forge URL containing the upload ISTK |
expires_at |
integer | Upload URL expiration timestamp |
Rename or Move Ingot
/v1/vaults/{vault_id}/ingots/{ingot_id}
Partially update an ingot: rename it and/or move it to a folder. Content is not modified.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Optional | New name (max 255 characters). Must not collide with another name in the target folder |
folder_id |
string|null | Optional | Target folder ID; null moves the ingot to the vault root |
At least one of name or folder_id is required; otherwise the request fails with 400 VALIDATION_ERROR. Returns the updated ingot object (same shape as List Ingots items).
Delete Ingot
/v1/vaults/{vault_id}/ingots/{ingot_id}
Permanently delete an ingot. This immediately frees pooled storage and stops it counting against your storage pool. Returns 204 No Content and cascade-deletes any SparkLink for the ingot.
Deleted ingots cannot be recovered. The encrypted data is permanently destroyed.
An ingot whose status is uploading and which is less than 1 hour old cannot be deleted: the request returns 400 VALIDATION_ERROR (prevents a race with Forge). A stale uploading ingot (older than 1 hour) can be deleted to clear a stuck upload.
Forge: Streaming Encryption
Forge is SparkVault's resumable encryption proxy. All cryptographic operations happen server-side using AES-256-GCM, eliminating the need for client-side crypto libraries.
- Resumable uploads: Recover failed transfers by resuming from the last accepted offset
- Bounded chunks: Upload bytes in a predictable memory envelope
- No client-side crypto: Encryption happens server-side over TLS
How It Works
Forge uses Ingot Secure Transfer Keys (ISTKs) for authentication. ISTKs are automatically embedded in the URLs returned by the Ingots API:
- Upload: The
forge_urlfrom Create Ingot includes the ISTK as a query parameter - Download: The
download_urlfrom Download Ingot includes the ISTK as a query parameter
You don't need to manually extract or manage ISTKs. Simply use the URLs returned by the API.
import * as tus from 'tus-js-client';
const forgeUrl = new URL(createResponse.data.forge_url);
const istk = forgeUrl.searchParams.get('istk');
const upload = new tus.Upload(file, {
endpoint: `${forgeUrl.origin}/encrypt`,
chunkSize: 50 * 1024 * 1024,
headers: { 'X-ISTK': istk },
metadata: {
filename: file.name,
filetype: file.type || 'application/octet-stream'
}
});
upload.start();
# The download_url already contains the ISTK
curl "$DOWNLOAD_URL" --output decrypted-file.tar.gz
See the Forge documentation for complete details on resumable encryption, including TUS protocol details and error handling.
Audit Logs
Per-ingot audit logs track uploads, downloads, sharing changes, and shared-link access. Audit logs are immutable and retained according to the vault's retention setting. For account- and vault-level audit logs, see the Audit Logs API.
/v1/vaults/{vault_id}/ingots/{ingot_id}/audit-logs
List audit events for an ingot.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit |
integer | Optional | Entries per page (1-100)Default: 25 |
cursor |
string | Optional | Signed pagination cursor from the previous response's next_cursor |
event_types |
string | Optional | Comma-separated event types to filter by (e.g. ingot_downloaded,ingot_deleted) |
Response
| Field | Type | Description |
|---|---|---|
entries |
array | Audit event entries, newest first |
entries[].event_type |
string | Event type (e.g. ingot_created, ingot_downloaded, sparklink_accessed) |
entries[].timestamp |
integer | Event timestamp |
entries[].actor_id |
string | User or API key that performed the action |
entries[].ip_address |
string | Client IP address |
entries[].user_agent |
string | Client user agent |
entries[].metadata |
object | Event-specific details. VAT downloads record name, size_bytes, content_type; SparkLink downloads record link_code_prefix, visibility, identity, access_mode |
count |
number | Number of entries in this page |
has_more |
boolean | Whether more entries are available |
next_cursor |
string? | Signed cursor for the next page (present when has_more is true) |
vault_id |
string | Vault identifier |
ingot_id |
string | Ingot identifier |
event_types |
array | All valid ingot event types, usable as event_types filter values |
Example
Requestcurl https://api.sparkvault.com/v1/vaults/vlt_abc123/ingots/ing_xyz789/audit-logs \
-H "X-API-Key: sv_live_xxx" \
-H "X-Vault-Access-Token: YOUR_VAT"
{
"data": {
"entries": [
{
"event_type": "ingot_downloaded",
"timestamp": 1704157200,
"actor_id": "usr_abc123",
"ip_address": "203.0.113.7",
"user_agent": "curl/8.5.0",
"metadata": {
"name": "report.pdf",
"size_bytes": 1048576,
"content_type": "application/pdf"
}
}
],
"count": 1,
"has_more": false,
"vault_id": "vlt_abc123",
"ingot_id": "ing_xyz789",
"event_types": [
"ingot_created", "ingot_renamed", "ingot_deleted", "ingot_moved",
"ingot_shared", "ingot_unshared", "invite_created", "invite_revoked",
"ingot_accessed", "ingot_downloaded", "ingot_uploaded",
"sparklink_accessed", "upload_error", "download_error"
]
}
}
Usage
Ingot operations consume the pooled storage and bandwidth included with your seat subscription. There is no per-GB fee; usage is tracked and counted against your pool. For subscription tiers and capacity blocks, see the pricing page.
Resource Usage
| Operation | Usage |
|---|---|
| Upload/Download | Counts against pooled bandwidth (included) |
| Storage | Counts against pooled storage (included) |
| Delete | Frees pooled storage |
When pooled storage or bandwidth is exhausted, extend it with a capacity block. See the pricing page for available blocks.
Error Reference
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Invalid request parameters, including size_bytes above the 5 TB maximum (size_bytes must be at most 5497558138880) and deleting an ingot whose upload is still in flight |
| 401 | AUTHENTICATION_ERROR |
Missing, malformed, or unknown Vault Access Token (X-Vault-Access-Token header), or a missing/invalid API key (X-API-Key) or session JWT (Authorization: Bearer) |
| 402 | PLAN_REQUIRED |
An active subscription is required for this operation |
| 402 | QUOTA_EXCEEDED |
Pooled storage or bandwidth exhausted (resource: storage|bandwidth); add a capacity block |
| 403 | FORBIDDEN |
VAT expired or bound to a different vault/account, read-only (Viewer) VAT on a write operation, or direct access to a managed ingot |
| 404 | NOT_FOUND |
Ingot not found (or name lookup failed) |
| 409 | CONFLICT |
Ambiguous name lookup (multiple ingots share the name) or target folder is being deleted |
| 412 | PRECONDITION_FAILED |
expected_updated_at did not match on PUT: the ingot changed; fetch the latest metadata and retry |