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.
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:
https://forge.sparkvault.com
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:
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"
}'
{
"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.
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"
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
/health
Check if the Forge service is operational. No authentication required.
curl https://forge.sparkvault.com/health
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2026-07-02T12:00:00.000Z"
}
Capability Discovery
/encrypt
tus capability discovery. Returns the supported tus version, protocol extensions, and maximum upload size as headers on a 204 response.
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
/encrypt
Create a tus resumable upload session. Upload plaintext with PATCH requests to the returned Location.
Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
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)
| Field | Type | Description |
|---|---|---|
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) |
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
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
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 |
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
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
/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)
| Field | Type | Description |
|---|---|---|
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
/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
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
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
| Field | Type | Description |
|---|---|---|
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 |
# The download_url from the Ingot download endpoint includes the ISTK
curl "https://forge.sparkvault.com/decrypt?istk=3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M" \
--output decrypted.pdf
curl "https://forge.sparkvault.com/decrypt?istk=3q2-7wX9kLmN0pQrStUvWxYzAb1Cd2Ef3Gh4Ij5Kl6M" \
-H "Range: bytes=1048576-2097151" \
--output part.bin
Check Progress
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
istk |
string | Required | The ISTK of the operation to inspect. Returns 404 if unknown. |
{
"success": true,
"progress": {
"bytes_downloaded": 52428800,
"bytes_processed": 52428800,
"total_bytes": 1073741824,
"status": "forging"
}
}
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
# 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
# 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
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
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:
- 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
- Forge validates the ISTK, derives the same KEK from KMS, and unwraps the data key, then the VAK
- The VAK is used directly as the AES-256-GCM key for the operation
- Isolation between ingots and chunks comes from the per-upload random base nonce and the per-chunk AAD binding, not from separate keys
- 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.
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
| Status | Code | Description |
|---|---|---|
| 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. |
{
"success": false,
"error": {
"code": "AUTHENTICATION_ERROR",
"message": "ISTK expired",
"details": {
"code": "ISTK_EXPIRED"
}
}
}
Best Practices
-
Check ISTK expiration before starting: Verify that
expires_atgives 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 withHEADbefore 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.