Forge - Resumable Encryption v1

Overview

Forge is SparkVault's encryption and transfer service, the data plane that every Ingot (an encrypted file stored in a Vault) passes through on its way into and out of storage. It is a high-performance encryption proxy that handles AES-256-GCM encryption and decryption server-side, so your client never touches key material or implements cryptography.

Uploads use TUS v1.0 resumable sessions with bounded PATCH chunks, so every file size (from a one-page PDF to a 5 TB archive) follows the same protocol and cryptographic format.

AES-256
GCM Encryption
TUS
Resumable Uploads
Server-Side
Cryptography
HTTPS
Transport Security
Zero Client-Side Crypto

Forge eliminates the need for client-side cryptography libraries. Send plaintext to Forge over HTTPS with TUS, and it handles encryption using industry-standard AES-256-GCM.

Base URL

Forge runs on a dedicated endpoint optimized for resumable encrypted transfer:

text
https://forge.sparkvault.com
Separate Service

Forge operates independently from the main API at api.sparkvault.com. It uses a different authentication mechanism (ISTK) and is optimized for high-throughput resumable operations.

Authentication

Forge uses Ingot Secure Transaction Keys (ISTKs) for authentication. An ISTK is a temporary, scoped credential generated when you access an Ingot in a Vault. It authorizes a specific cryptographic operation (encrypt or decrypt) for a limited time.

An ISTK is an opaque, base64url-encoded 32-byte random value. It carries no embedded claims. All context (account, vault, ingot, operation, key material) lives in a server-side record that Forge looks up when the token is presented.

How to Obtain an ISTK

The Ingot endpoints issue ISTKs. Creating an ingot (POST /v1/vaults/{vault_id}/ingots) returns a forge_url for encryption; initiating a download (POST /v1/vaults/{vault_id}/ingots/{ingot_id}/download) returns a download_url for decryption. Both URLs carry the ISTK as the istk query parameter:

bash Create Ingot to Get a Forge URL
curl -X POST https://api.sparkvault.com/v1/vaults/{vault_id}/ingots \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: YOUR_VAT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "large-backup.tar.gz",
    "size_bytes": 1073741824,
    "content_type": "application/gzip"
  }'
json Response with Forge URL
{
  "data": {
    "ingot_id": "ing_abc123...",
    "name": "large-backup.tar.gz",
    "size_bytes": 1073741824,
    "content_type": "application/gzip",
    "status": "uploading",
    "forge_url": "https://forge.sparkvault.com/encrypt?istk=3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M",
    "expires_at": 1782172800
  }
}

Single-Use vs Streaming ISTKs

Encrypt ISTKs and default decrypt ISTKs are single-use: Forge atomically claims the token the first time it is presented, and any reuse fails with 401 AUTHENTICATION_ERROR (details.code: ISTK_USED). In practice this means a download_url works exactly once.

For media playback (where a player issues many Range requests against one URL), pass { "stream": true } in the body of the Ingot download request (POST /v1/vaults/{vault_id}/ingots/{ingot_id}/download). This mints a reusable streaming-decrypt URL that is not consumed on use. Its blast radius is bounded by a cumulative byte cap and a sliding idle TTL (see the expiration note below).

Using the ISTK with Forge

Extract the ISTK from the returned forge_url and pass it in the X-ISTK header when creating the upload session (POST /encrypt). That is where the token is validated and atomically consumed. Subsequent PATCH/HEAD/DELETE requests are keyed by the upload ID from the Location header and do not read X-ISTK (sending it is harmless but unnecessary). For decryption, the ISTK rides along as the istk query parameter already embedded in the download_url.

bash
curl -i -X POST https://forge.sparkvault.com/encrypt \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: $(wc -c < plaintext.file)" \
  -H "X-ISTK: 3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M"
ISTK Expiration

Single-use ISTKs have a fixed 24-hour expiry. Reusable streaming-decrypt URLs have a 1-hour sliding idle TTL (refreshed on each use) with a 12-hour absolute cap. Always check expires_at and obtain a fresh Forge URL if needed before starting a Forge operation.

API Reference

Health Check

GET /health

Check if the Forge service is operational. No authentication required.

bash Request
curl https://forge.sparkvault.com/health
json Response
{
  "status": "healthy",
  "version": "1.0.0",
  "timestamp": "2026-07-02T12:00:00.000Z"
}

Capability Discovery

OPTIONS /encrypt

tus capability discovery. Returns the supported tus version, protocol extensions, and maximum upload size as headers on a 204 response.

text Response Headers
Tus-Resumable: 1.0.0
Tus-Version: 1.0.0
Tus-Extension: creation,termination
Tus-Max-Size: 5497558138880

Tus-Max-Size is 5,497,558,138,880 bytes (5 TB), the maximum size of a single ingot.

Create Upload Session

POST /encrypt

Create a tus resumable upload session. Upload plaintext with PATCH requests to the returned Location.

Headers

ParameterTypeRequiredDescription
Tus-Resumable string Required Must be 1.0.0
Upload-Length integer Required Total plaintext size in bytes. Rejected with 400 if it exceeds the size cap embedded in the ISTK.
X-ISTK string Required Single-use encrypt ISTK from the Forge URL. Validated and atomically consumed by this request.

Response (201 Created)

FieldTypeDescription
Location header Upload path for PATCH chunks (/encrypt/{upload_id}). Resolve it against the Forge origin.
Upload-Offset header Current offset, initially 0
Upload-Length header Echo of the declared total plaintext size
Tus-Resumable header 1.0.0
X-Chunk-Size header Maximum PATCH chunk size: 52,428,800 bytes (50 MB)
bash Create a tus Upload
curl -i -X POST https://forge.sparkvault.com/encrypt \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: $(wc -c < plaintext.pdf)" \
  -H "X-ISTK: 3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M"

Upload Chunk

PATCH /encrypt/{upload_id}

Upload the next plaintext chunk to the session. Returns 204 No Content with the new Upload-Offset header. No X-ISTK required: the session is keyed by the upload ID.

Headers

ParameterTypeRequiredDescription
Tus-Resumable string Required Must be 1.0.0
Upload-Offset integer Required Must equal the server's current offset, or the chunk is rejected with 400
Content-Type string Required Must be application/offset+octet-stream
bash Upload a Chunk
curl -X PATCH https://forge.sparkvault.com/encrypt/{upload_id} \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Offset: 0" \
  -H "Content-Type: application/offset+octet-stream" \
  --upload-file plaintext.pdf
Fixed Chunk Geometry

Every non-final chunk must be exactly X-Chunk-Size (50 MB). Only the final remainder may be smaller. Undersized non-final chunks, chunks larger than X-Chunk-Size, and chunks that would push past the declared Upload-Length are all rejected with 400. If two requests race on the same offset, the loser receives 409 CONFLICT. Re-check the offset with HEAD and retry.

Check Upload Offset

HEAD /encrypt/{upload_id}

tus offset check. Returns 200 OK with the session's current state in headers. Use it to find where to resume.

Response (200 OK)

FieldTypeDescription
Upload-Offset header Bytes received and committed so far
Upload-Length header Declared total plaintext size
X-Chunk-Size header Negotiated maximum PATCH chunk size

Cancel Upload

DELETE /encrypt/{upload_id}

Cancel an in-progress upload. Deletes the staged encrypted chunks, bills the partial transfer, and returns 204 No Content. A no-op if the upload already finalized.

Decrypt Stream

GET /decrypt

Decrypt an ingot that was previously encrypted with Forge. The download URL (including ISTK) is provided by the Ingot download endpoint. Returns the plaintext stream in the response. Default download URLs are single-use.

Query Parameters

ParameterTypeRequiredDescription
istk string Required Ingot Secure Transaction Key (included in the download_url from the Ingot download endpoint). May alternatively be sent as an X-ISTK header; the query parameter takes precedence.

GET /decrypt honors single-range Range: bytes= requests, returning 206 Partial Content with a Content-Range header (or 416 when the range is unsatisfiable). This is the basis for resumable/partial downloads and media seeking with reusable streaming URLs.

Response

FieldTypeDescription
Body binary The decrypted plaintext stream
Content-Type header The ingot's stored content type (falls back to application/octet-stream)
Content-Disposition header attachment with a filename derived from the ingot
Content-Length header Bytes served (the full size, or the range length for 206 responses)
Accept-Ranges header bytes
X-Ingot-ID header The ingot being decrypted
Cache-Control header private, no-store: decrypted plaintext is never cacheable
bash Decrypt a File
# The download_url from the Ingot download endpoint includes the ISTK
curl "https://forge.sparkvault.com/decrypt?istk=3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M" \
  --output decrypted.pdf
bash Partial Download with a Byte Range
curl "https://forge.sparkvault.com/decrypt?istk=3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M" \
  -H "Range: bytes=1048576-2097151" \
  --output part.bin

Check Progress

GET /progress

Poll transfer progress for an ISTK-scoped operation. Reading progress does not consume the ISTK, so it is safe to poll during a transfer.

Query Parameters

ParameterTypeRequiredDescription
istk string Required The ISTK of the operation to inspect. Returns 404 if unknown.
json Response
{
  "success": true,
  "progress": {
    "bytes_downloaded": 52428800,
    "bytes_processed": 52428800,
    "total_bytes": 1073741824,
    "status": "forging"
  }
}
HEAD Preflight

A HEAD request to /decrypt, /encrypt, or /progress returns 200 without consuming the ISTK, useful for availability checks before committing a single-use token.

Examples

Encrypt and Store a File

bash Complete Workflow
# 1. Create the ingot - the response's forge_url carries the ISTK
RESPONSE=$(curl -s -X POST https://api.sparkvault.com/v1/vaults/vlt_xxx/ingots \
  -H "X-API-Key: $API_KEY" \
  -H "X-Vault-Access-Token: $VAT" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"backup.tar.gz\",
    \"size_bytes\": $(wc -c < backup.tar.gz),
    \"content_type\": \"application/gzip\"
  }")

FORGE_URL=$(echo $RESPONSE | jq -r '.data.forge_url')
ISTK="${FORGE_URL#*istk=}"

# 2. Create a TUS upload session (the only request that needs X-ISTK)
HEADER_FILE=$(mktemp)
curl -s -D "$HEADER_FILE" -o /dev/null -X POST https://forge.sparkvault.com/encrypt \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: $(wc -c < backup.tar.gz)" \
  -H "X-ISTK: $ISTK"

# Location is a path ("/encrypt/{upload_id}") - resolve it against the Forge origin
UPLOAD_URL="https://forge.sparkvault.com$(awk 'tolower($1) == "location:" { print $2 }' "$HEADER_FILE" | tr -d '\r')"

# 3. Upload plaintext through the TUS session. A file up to 50 MB fits in a
#    single PATCH; larger files send one PATCH per 50 MB chunk (see below).
curl -X PATCH "$UPLOAD_URL" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Offset: 0" \
  -H "Content-Type: application/offset+octet-stream" \
  --upload-file backup.tar.gz

Resumable Uploads

bash Resume an Upload
# Ask Forge where to resume (keyed by the upload URL - no ISTK needed)
OFFSET=$(curl -sI "$UPLOAD_URL" \
  -H "Tus-Resumable: 1.0.0" | awk 'tolower($1) == "upload-offset:" { print $2 }' | tr -d '\r')

# Continue at that offset with the next plaintext chunk (non-final chunks
# must be exactly 50 MB = 52428800 bytes)
tail -c +$((OFFSET + 1)) backup.tar.gz | head -c 52428800 > chunk.bin
curl -X PATCH "$UPLOAD_URL" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Offset: $OFFSET" \
  -H "Content-Type: application/offset+octet-stream" \
  --upload-file chunk.bin

JavaScript / Node.js

javascript
import * as tus from 'tus-js-client';

function uploadWithForge(file, forgeUrl) {
  const url = new URL(forgeUrl);
  const istk = url.searchParams.get('istk');

  const upload = new tus.Upload(file, {
    endpoint: `${url.origin}/encrypt`,
    chunkSize: 50 * 1024 * 1024,
    // tus-js-client sends headers on every request; Forge only requires
    // X-ISTK on session creation and ignores it elsewhere.
    headers: { 'X-ISTK': istk },
    retryDelays: [0, 1000, 3000, 5000, 10000],
    metadata: {
      filename: file.name,
      filetype: file.type || 'application/octet-stream'
    }
  });

  upload.start();
}

Python

python
import os
from urllib.parse import parse_qs, urljoin, urlparse

import requests

CHUNK_SIZE = 50 * 1024 * 1024

def upload_file(forge_url: str, input_path: str):
    """Upload a file through Forge with TUS."""
    parsed = urlparse(forge_url)
    istk = parse_qs(parsed.query)['istk'][0]
    endpoint = f'{parsed.scheme}://{parsed.netloc}/encrypt'
    size = os.path.getsize(input_path)

    # X-ISTK is only needed here, on session creation
    create = requests.post(
        endpoint,
        headers={
            'Tus-Resumable': '1.0.0',
            'Upload-Length': str(size),
            'X-ISTK': istk,
        },
    )
    create.raise_for_status()
    upload_url = urljoin(endpoint, create.headers['Location'])

    offset = 0
    with open(input_path, 'rb') as f:
        while offset < size:
            chunk = f.read(CHUNK_SIZE)
            patch = requests.patch(
                upload_url,
                headers={
                    'Tus-Resumable': '1.0.0',
                    'Upload-Offset': str(offset),
                    'Content-Type': 'application/offset+octet-stream',
                },
                data=chunk,
            )
            patch.raise_for_status()
            offset = int(patch.headers['Upload-Offset'])


def decrypt_file(download_url: str, output_path: str):
    """Decrypt a file through Forge.

    The download_url is provided by the Ingot download endpoint
    and includes the ISTK as a query parameter. Default download
    URLs are single-use.
    """
    response = requests.get(download_url, stream=True)
    response.raise_for_status()

    with open(output_path, 'wb') as out:
        for chunk in response.iter_content(chunk_size=8192):
            out.write(chunk)

    print(f'Decrypted: {output_path}')


# Usage
upload_file(forge_url, 'report.xlsx')
decrypt_file(download_url, 'report-restored.xlsx')

Common Use Cases

Database Backups

Encrypt database dumps before storing them as ingots, using the same resumable upload flow as every other file.

Video Content Protection

Encrypt video files of any size for DRM or content protection before distribution.

Log Encryption

Encrypt log files containing sensitive data before archival to meet compliance requirements.

File Transfer Security

Add end-to-end encryption to file transfers without modifying client applications.

IoT Data Encryption

Encrypt sensor data and telemetry from devices that lack cryptographic capabilities.

Medical Imaging

Encrypt DICOM files and medical images for HIPAA-compliant storage.

Security Details

Encryption Algorithm

Forge uses AES-256-GCM (Galois/Counter Mode) for authenticated chunk encryption:

  • Key Size: 256 bits (32 bytes), the vault-level Vault Access Key (VAK)
  • IV/Nonce: a random 96-bit base nonce per upload; each chunk's nonce is derived deterministically as base nonce XOR chunk index
  • Authentication Tag: 128 bits (16 bytes) per chunk for integrity verification
  • Additional Authenticated Data: every chunk's AAD binds the account, vault, ingot, and chunk index, so ciphertext cannot be replayed across ingots or reordered within one
  • Mode: Galois/Counter Mode provides both confidentiality and authenticity

Key Handling

Forge never derives or stores long-lived keys of its own. The key arrives with the ISTK:

  1. The Core API envelope-encrypts the vault's VAK into the ISTK record: a KMS-derived key-encryption key wraps a random data key, which wraps the VAK
  2. Forge validates the ISTK, derives the same KEK from KMS, and unwraps the data key, then the VAK
  3. The VAK is used directly as the AES-256-GCM key for the operation
  4. Isolation between ingots and chunks comes from the per-upload random base nonce and the per-chunk AAD binding, not from separate keys
  5. The VAK is zeroized from memory as soon as the operation completes or fails

Transport Security

All traffic to and from Forge is carried over HTTPS. TLS terminates at the platform edge before requests reach the service, so plaintext never crosses the public internet unencrypted.

Defense in Depth

Data is protected by two layers of encryption: TLS during transport to Forge, and AES-256-GCM for the stored ciphertext. Even if transport encryption were compromised, the encrypted data remains secure.

Error Responses

Error Responses

StatusCodeDescription
400 VALIDATION_ERROR Missing tus headers or a missing istk on /decrypt (details.code: MISSING_HEADERS), a missing X-ISTK header, an ISTK presented for the wrong operation, an Upload-Offset mismatch, an oversize or undersized chunk, or an Upload-Length exceeding the ISTK's size cap.
401 AUTHENTICATION_ERROR ISTK rejected. details.code is one of INVALID_ISTK (unknown or malformed), ISTK_EXPIRED, or ISTK_USED (single-use token already claimed). Obtain a fresh Forge URL from the ingot create or download endpoint.
403 (empty body) A reusable streaming-decrypt URL whose cumulative byte cap is exhausted. The response has an empty body (no JSON error envelope) and the token is revoked, so subsequent requests fail with 401 AUTHENTICATION_ERROR (details.code: ISTK_USED).
404 NOT_FOUND Upload session not found, or the target ingot is missing or not active.
409 CONFLICT A concurrent PATCH lost the exclusive write lease, or finalization is in progress. Re-check the offset with HEAD and retry.
500 CRYPTOGRAPHIC_ERROR Encryption or decryption failed: authentication-tag mismatch or corrupted key material.
500 STORAGE_ERROR Object-storage upload or download failed. Retry the chunk at the same offset.
500 INTERNAL_ERROR Unexpected server error. Retry with exponential backoff.
json Example Error Response
{
  "success": false,
  "error": {
    "code": "AUTHENTICATION_ERROR",
    "message": "ISTK expired",
    "details": {
      "code": "ISTK_EXPIRED"
    }
  }
}

Best Practices

  • Check ISTK expiration before starting: Verify that expires_at gives enough time to complete your operation. Obtain a fresh Forge URL if needed.
  • Use tus clients: Keep upload logic on the resumable tus path and send bounded PATCH chunks with Upload-Offset.
  • Store metadata separately: Forge only encrypts ingot bytes. Store filename, content type, and other metadata in SparkVault ingot metadata.
  • Implement retry logic: Retry with exponential backoff on transient network failures. On 409 CONFLICT, re-check the current offset with HEAD before retrying.
  • Verify with checksums: Calculate checksums of plaintext before encryption and after decryption to verify data integrity end-to-end.
  • Use separate Vaults for different data types: Create distinct Vaults for different data categories (backups, documents, media). Each Vault has its own VAK, giving you key isolation and independent access control.

Usage

Forge operations consume pooled bandwidth included with your seat subscription. Both encrypt and decrypt operations count the processed data size against your pooled bandwidth. There is no per-GB charge. When pooled bandwidth is exhausted, extend it with a capacity block.

For subscription tiers and capacity blocks, see the Pricing page.