Ingots API v1

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.

Looking for key/value storage?

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
VAT Required

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.

Name-Based Addressing

{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

POST /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

ParameterTypeRequiredDescription
X-Vault-Access-Token string Required Valid VAT from unseal operation

Request Body

ParameterTypeRequiredDescription
name string Required File name or identifier
size_bytes integer Required Exact file size in bytes (1 byte to 5 TB = 5497558138880 bytes); the upload may send no more. What the upload adds to the storage pool is reserved for it while it is in progress: this size, less the size of the content it replaces when it upserts onto an existing ingot
content_type string Optional MIME typeDefault: application/octet-stream
original_filename string Optional The name of the local file the upload came from, recorded with the ingot. A download saves under the ingot's current name
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
on_name_conflict string Optional What happens when the name already exists in the target folder: upsert, complete_pending, refuse or new_name (see Upsert by Name)Default: upsert
Upsert by Name

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. The new content's content_type and original_filename replace the ingot's, and an original_filename the upload does not send is cleared rather than carried over from the content it replaces.

Send on_name_conflict: "complete_pending" to create without ever replacing content: an existing ingot whose upload never completed is reused so the upload can finish, and any other same-name ingot answers 409 CONFLICT and is left unchanged.

Send on_name_conflict: "refuse" to create only a new ingot: any same-name ingot, its upload finished or not, answers 409 CONFLICT and is left unchanged.

A 409 from complete_pending or refuse names the ingot holding the name: error.details is { "reason": "NAME_HELD", "ingot_id", "status", "size_bytes" }, so the caller can act on it without reading it again.

Send on_name_conflict: "new_name" to keep both: the ingot is created under the first free numbered name (report (2).pdf), returned as the response's name.

When two uploads of the same name overlap (a create retried after its first answer was lost, a second device uploading the same file), the later one is the upload the ingot keeps. The earlier one's Forge URL stops working the moment the later create is made: Forge answers it, and every request on an upload session it opened, 410 UPLOAD_GONE naming UPLOAD_SUPERSEDED. An earlier create the later one overtook before its URL was issued answers 410 UPLOAD_SUPERSEDED itself, with no URL. No new upload remedies either: the later one holds the file.

Response

FieldTypeDescription
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
javascript Complete Upload Example (JavaScript)
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. Forge requires every non-final
// chunk to be exactly X-Chunk-Size (52,428,800 bytes); tus-js-client cannot
// read that header, so the value is pinned.
const forgeUrl = new URL(forge_url);
const istk = forgeUrl.searchParams.get('istk');
const upload = new tus.Upload(file, {
  endpoint: `${forgeUrl.origin}/encrypt`,
  chunkSize: 52428800,
  headers: { 'X-ISTK': istk },
  retryDelays: [0, 1000, 3000, 5000, 10000]
});

upload.start();

// Status will change: uploading → active

List Ingots

GET /v1/vaults/{vault_id}/ingots

List all ingots in an unsealed vault.

Query Parameters

ParameterTypeRequiredDescription
limit integer Optional Ingots per page, at most 200; a larger value reads as 200 (Pagination)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

FieldTypeDescription
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 until Forge activates the upload that created the ingot, then active. Replacing an active ingot's content leaves it active
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); 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

GET /v1/vaults/{vault_id}/ingots/search

Search and filter ingots within a vault using indexed queries. Filters can be combined.

Query Parameters

ParameterTypeRequiredDescription
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 Results per page, at most 200; a larger value reads as 200 (Pagination)Default: 50
cursor string Optional Sealed pagination cursor from the previous response's next_cursor
sort_order string Optional asc or descDefault: desc

Response

FieldTypeDescription
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? Sealed 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
bash Example Searches
# 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

GET /v1/vaults/{vault_id}/ingots/{ingot_id}

Retrieve an ingot's metadata. Use the download endpoint to retrieve decrypted contents.

Response

FieldTypeDescription
ingot_id string Ingot identifier
name string Ingot name
size_bytes integer Size in bytes
content_type string MIME type
status string Status: uploading until Forge activates the upload that created the ingot, then active. Replacing an active ingot's content leaves it active
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 that replaces the whole content (a replacement built from downloaded content uses expected_content_version instead)
access_count integer Number of downloads
total_bandwidth_bytes integer Total bytes served for this ingot

Download Ingot

POST /v1/vaults/{vault_id}/ingots/{ingot_id}/download

Get a time-limited download URL for decrypting an ingot via Forge.

Request Body

ParameterTypeRequiredDescription
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

FieldTypeDescription
status string Status (always ready)
download_url string Signed Forge URL for streaming decryption (includes ISTK)
ingot_id string Ingot identifier
name string The ingot's current name, which the download saves under
size_bytes integer File size in bytes
content_type string MIME type
original_filename string The name of the local file the content was uploaded from
expires_at integer Download URL expiration timestamp
stream boolean Whether the URL is a reusable streaming URL
javascript Download Example
// 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 } = (await downloadResponse.json()).data;

// Step 2: Open the Forge URL. Forge streams the decrypted file and names it
// (Content-Disposition) under the ingot's current name, with path separators,
// control characters and bidi controls replaced.
window.location.href = download_url;

Replace Ingot Content

PUT /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. The ingot keeps its original_filename. A replacement another upload of the ingot overtook before its URL was issued answers 410 UPLOAD_SUPERSEDED, with no URL.

Request Body

ParameterTypeRequiredDescription
size_bytes integer Required Exact size of the replacement content in bytes (1 byte to 5 TB); the upload may send no more. What the upload adds to the storage pool is reserved for it while it is in progress: this size less the size of the current content, nothing when it is no larger
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. Suited to a replacement that does not depend on the current content
expected_content_version string|null Optional For a read-modify-write (download, edit, replace): the X-Content-Version header of the Forge download whose bytes you edited, or null if it carried none. The PUT fails with 412 PRECONDITION_FAILED if that content has since been replaced or another replacement of the ingot is still in flight, and the upload's final PATCH is refused with 412 if the content is replaced before it publishes. Use it instead of expected_updated_at here: an ingot keeps serving its old content until a replacement publishes, so updated_at can move ahead of the bytes you downloaded

Response

FieldTypeDescription
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

PATCH /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

ParameterTypeRequiredDescription
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

DELETE /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.

Irreversible

Deleted ingots cannot be recovered. The encrypted data is permanently destroyed.

Deleting During an Upload

An ingot can be deleted while an upload to it is in progress: the delete revokes that upload's Forge URL and cancels the upload, so Forge answers the URL, and every request on its upload session, 410 UPLOAD_GONE (its X-Upload-Refusal header naming NOT_FOUND: the file it was for no longer exists), and the delete releases the storage the upload reserved. A create or replacement whose ingot is deleted before its Forge URL is issued answers 404 NOT_FOUND.

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.

Why Use Forge
  • 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_url from Create Ingot includes the ISTK as a query parameter
  • Download: The download_url from Download Ingot includes the ISTK as a query parameter

You don't need to manually extract or manage ISTKs. Use the URLs the API returns.

javascript Upload via Forge (using forge_url from Create Ingot)
import * as tus from 'tus-js-client';

const forgeUrl = new URL(createResponse.data.forge_url);
const istk = forgeUrl.searchParams.get('istk');

// Every non-final chunk must be exactly Forge's X-Chunk-Size (52,428,800 bytes).
const upload = new tus.Upload(file, {
  endpoint: `${forgeUrl.origin}/encrypt`,
  chunkSize: 52428800,
  headers: { 'X-ISTK': istk },
  metadata: {
    filename: file.name,
    filetype: file.type || 'application/octet-stream'
  }
});

upload.start();
bash Download via Forge (using download_url from Download Ingot)
# 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.

GET /v1/vaults/{vault_id}/ingots/{ingot_id}/audit-logs

List audit events for an ingot.

Query Parameters

ParameterTypeRequiredDescription
limit integer Optional Entries per page, at most 100; a larger value reads as 100 (Pagination)Default: 25
cursor string Optional Sealed 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_renamed)

Response

FieldTypeDescription
entries array Audit event entries, newest first
entries[].event_type string Event type (e.g. ingot_created, ingot_downloaded)
entries[].timestamp integer Event timestamp
entries[].actor_id string? Who performed the action, as in the audit log entry fields: a user (usr_...), or the API key (key_...) a request presented, the request to a SparkVault product such as Structured Ingots included; null for a download through a shared link, whose metadata names the link and any verified identity, and for a file uploaded through the vault's public upload page (ingot_uploaded)
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 (the link's masked code: its first six characters and ***), visibility, identity, access_mode
count number Number of entries in this page
has_more boolean Whether more entries are available
next_cursor string? Sealed 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

Request
bash
curl 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"
Response
json
{
  "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_uploaded", "ingot_downloaded", "ingot_renamed", "ingot_moved"
    ]
  }
}

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
Extending Capacity

When pooled storage or bandwidth is exhausted, extend it with a capacity block. See the pricing page for available blocks.

Error Reference

Error Responses

StatusCodeDescription
400 VALIDATION_ERROR Invalid request parameters, including size_bytes above the 5 TB maximum (size_bytes must be at most 5497558138880)
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, or what an upload adds to the storage pool (its size_bytes, less the size of the content it replaces) does not fit beside what the account stores and what its other uploads in progress reserve (resource: storage|bandwidth; a refused upload names required_bytes and reserved_bytes); 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), or deleted before a create or replacement of it could issue its Forge URL
409 CONFLICT Ambiguous name lookup (multiple ingots share the name), target folder is being deleted, or the ingot kept changing while a delete was decided (retry the delete)
410 UPLOAD_SUPERSEDED A create or replacement another upload of the same ingot overtook before its Forge URL was issued (a create retried after its first answer was lost, a second device uploading the same file): the later upload is the one the ingot keeps, and no URL is issued. A new upload is no remedy
412 PRECONDITION_FAILED A PUT precondition no longer holds: expected_updated_at did not match, or the content named by expected_content_version was replaced or another replacement of the ingot is still in flight. Re-read the ingot (re-download it for a read-modify-write) and retry
429 UPLOAD_LIMIT The account already has 500 uploads in progress: finish or cancel one. A slot frees the moment any of them ends, which no time predicts, so Retry-After asks for a retry in 60 seconds