Sparks API v1

Overview

Sparks are ephemeral, self-destructing secrets. When someone reads a Spark, the plaintext is returned and the Spark is immediately destroyed ("burned"). The encrypted data can never be accessed again.

Key Features

  • Read-Once: Data is permanently destroyed after first read
  • Auto-Expiration: Unread Sparks expire after TTL (default 24 hours, max 24 hours)
  • Double Zero-Trust: Encrypted with both system and account keys
  • Audit Trail: Track when Sparks are created, read, and destroyed

Content Types

Sparks support both text and binary content. The server treats text/* types (plus application/json, application/xml, application/javascript, and types with +json/+xml suffixes) as text: send those payloads as UTF-8 strings. Any other content type is treated as binary: base64-encode the payload and set content_type to the appropriate MIME type.

Content Type Encoding Example
text/plain UTF-8 string Passwords, API keys, text
application/pdf Base64 PDF documents
application/json UTF-8 string JSON configuration
application/octet-stream Base64 Generic binary files
text/uri-list UTF-8 string (a single http/https URL) Link Sparks: burn-on-open redirect links

Spark States

Status Description
active Spark is available and has not been read yet
reading Transient: an atomic read lock is held while a burn-read is in flight. Concurrent readers receive 409 CONFLICT; a stale lock recovers after 30 seconds
ash Spark has been read and destroyed ("burned")
expired Spark TTL elapsed without being read

Create Spark

POST /v1/sparks

Create a new ephemeral secret. The payload is encrypted and can only be read once.

Request Body

ParameterTypeRequiredDescription
payload string Required The secret data to encrypt (max 256 KB / 256,000 bytes). For binary content, send as base64; the 256,000-byte limit applies to the base64-encoded payload (roughly 192 KB of raw binary), while size_bytes reports the decoded size. For link Sparks (text/uri-list), a single http/https URL.
ttl_minutes integer Optional Time-to-live in minutes (1-1440, max 24 hours)Default: 1440 (24 hours)
content_type string Optional MIME type of payload. Types the server does not treat as text indicate base64-encoded binary. text/uri-list creates a link Spark (see below).Default: text/plain
filename string Optional Original filename for file uploads (preserved for download)Default: null
with_kindling boolean Optional Generate a new kindling ID for grouping related SparksDefault: false
kindling string Optional Join an existing kindling group (e.g., kdl_xxx)Default: null
bound_ip string Optional Link Sparks only: bind the auto-minted x.sv link to a single IP addressDefault: null

Response

FieldTypeDescription
spark_id string Unique spark identifier (spk_...)
size_bytes integer Payload size in bytes
status string Status (active)
created_at integer Unix timestamp of creation
expires_at integer Unix timestamp when Spark expires
ttl_minutes integer TTL in minutes
filename string? Original filename (only present if provided for file uploads)
kindling string? Kindling ID (only present if with_kindling or kindling was provided)
link_url string? Link Sparks only: shareable https://x.sv/{code} URL
link_code string? Link Sparks only: SparkLink code of the minted link

Basic Example

Request
bash
curl -X POST https://api.sparkvault.com/v1/sparks \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "super_secret_password_123",
    "ttl_minutes": 60
  }'
Response
json
{
  "data": {
    "spark_id": "spk_abc123def456...",
    "size_bytes": 25,
    "status": "active",
    "created_at": 1702000000,
    "expires_at": 1702003600,
    "ttl_minutes": 60
  }
}

Link Sparks (text/uri-list)

Setting content_type to text/uri-list creates a link Spark: the payload must be a single http/https URL (validated before sealing), and the destination is encrypted at rest, never stored in plaintext. The server automatically mints a public x.sv SparkLink for the Spark, so the response additionally carries link_url (https://x.sv/{code}) and link_code. The destination burns on first open. Pass bound_ip to bind the link to a single IP address. List responses surface link_url for link Sparks.

Create a Link Spark

Request
bash
curl -X POST https://api.sparkvault.com/v1/sparks \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "https://example.com/one-time-download",
    "content_type": "text/uri-list",
    "ttl_minutes": 60
  }'
Response
json
{
  "data": {
    "spark_id": "spk_abc123...",
    "size_bytes": 38,
    "status": "active",
    "created_at": 1702000000,
    "expires_at": 1702003600,
    "ttl_minutes": 60,
    "link_url": "https://x.sv/abc123...",
    "link_code": "abc123..."
  }
}

List Sparks

GET /v1/sparks

List Sparks created by your account. Supports filtering by status and cursor-based pagination.

Query Parameters

ParameterTypeRequiredDescription
status string Optional Exact-match filter on stored status: active or ash. Omit the parameter to return all Sparks. (expired is computed per item in the response, not a filterable stored status.)Default: none (all Sparks)
kindling string Optional Filter by kindling group (e.g., kdl_xxx)Default: null
limit integer Optional Maximum results per page (1-500)Default: 100
cursor string Optional Opaque signed pagination cursor from a previous response's next_cursorDefault: null

Response

FieldTypeDescription
sparks array Array of Spark objects
sparks[].spark_id string Spark identifier
sparks[].account_id string Owning account ID
sparks[].content_type string MIME type of payload
sparks[].filename string? Original filename (if provided for file uploads)
sparks[].status string Current status: active, ash, or expired (computed at response time)
sparks[].size_bytes integer Payload size
sparks[].created_at integer Creation timestamp
sparks[].expires_at integer Expiration timestamp
sparks[].burned_at integer? When read (null unless burned)
sparks[].time_remaining integer Seconds until expiration (0 if expired)
sparks[].kindling string? Kindling group ID (kdl_...), when the Spark belongs to a kindling group
sparks[].link_url string? Shareable https://x.sv/{code} URL (link Sparks only)
count integer Number of Sparks in this page
has_more boolean Whether more pages exist
next_cursor string? Signed opaque cursor for the next page (present when has_more is true); pass it back as ?cursor=

Filter Active Sparks

Request
bash
curl "https://api.sparkvault.com/v1/sparks?status=active&limit=10" \
  -H "X-API-Key: sv_live_xxx"
Response
json
{
  "data": {
    "sparks": [
      {
        "spark_id": "spk_abc123...",
        "account_id": "acc_xyz789...",
        "content_type": "text/plain",
        "status": "active",
        "size_bytes": 25,
        "created_at": 1702000000,
        "expires_at": 1702086400,
        "burned_at": null,
        "time_remaining": 82800
      }
    ],
    "count": 1,
    "has_more": false
  }
}

Read Spark (Burn)

GET /v1/sparks/{spark_id}

Read and destroy a Spark. The decrypted payload is returned and the Spark is immediately burned. This operation cannot be undone.

Response

FieldTypeDescription
spark_id string Spark identifier
payload string The decrypted secret data (base64 if the content_type is not treated as text)
content_type string MIME type. Types the server does not treat as text (see Content Types) indicate the payload is base64-encoded.
filename string? Original filename (if provided for file uploads)
size_bytes integer Payload size
status string Status (now ash)
created_at integer Creation timestamp
burned_at integer Timestamp when burned
was_active boolean Always true on a successful burn-read

Read Standard Spark

Request
bash
curl https://api.sparkvault.com/v1/sparks/spk_abc123 \
  -H "X-API-Key: sv_live_xxx"
Response
json
{
  "data": {
    "spark_id": "spk_abc123def456...",
    "payload": "super_secret_password_123",
    "content_type": "text/plain",
    "size_bytes": 25,
    "status": "ash",
    "created_at": 1702000000,
    "burned_at": 1702001000,
    "was_active": true
  }
}
Destructive Operation

Reading a Spark permanently destroys the encrypted data. The payload is returned exactly once. There is no way to read a Spark again after this call.

Attempting to read an already-burned Spark returns 403 FORBIDDEN ("Spark has already been burned (read once)"), and an expired Spark returns 403 FORBIDDEN ("Spark has expired"). Only a nonexistent spark_id returns 404 NOT_FOUND. A concurrent in-flight read returns 409 CONFLICT.

Error Responses

StatusCodeDescription
403 FORBIDDEN Spark already burned or expired; also returned when a concurrent request wins the burn race
404 NOT_FOUND Spark does not exist
409 CONFLICT Spark is currently being read by another request

Delete Spark

DELETE /v1/sparks/{spark_id}

Delete an unread Spark without reading its contents. Use this to cancel a Spark before it's read. Returns 204 No Content on success.

Example

Request
bash
curl -X DELETE https://api.sparkvault.com/v1/sparks/spk_abc123 \
  -H "X-API-Key: sv_live_xxx"
Response
text
(204 No Content)
When to Delete

Use this if you created a Spark by mistake or need to revoke access before the recipient reads it. Deletion is idempotent: deleting an already-burned Spark returns 204 as a no-op, and burned (ash) Sparks remain visible in listings until their natural expiry. Deleting a Spark also cascade-removes any SparkLink attached to it.

Kindling (Grouping Sparks)

Kindling allows you to group related Sparks together. This is useful for multi-step workflows like verification flows, retry sequences, or any scenario where multiple Sparks belong to the same logical operation.

How Kindling Works

  • Start a Group: Create a Spark with with_kindling: true to generate a new kindling ID
  • Join a Group: Create subsequent Sparks with kindling: "kdl_xxx" to add them to the family
  • Query by Kindling: List all Sparks in a group using ?kindling=kdl_xxx

Kindling Parameters (Create Spark)

ParameterTypeRequiredDescription
with_kindling boolean Optional Set to true to generate a new kindling ID for this SparkDefault: false
kindling string Optional Existing kindling ID to join (e.g., kdl_abc123)Default: null

Create Spark with New Kindling

Request
bash
curl -X POST https://api.sparkvault.com/v1/sparks \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "verification code data",
    "ttl_minutes": 15,
    "with_kindling": true
  }'
Response
json
{
  "data": {
    "spark_id": "spk_abc123...",
    "kindling": "kdl_xyz789...",
    "status": "active",
    "created_at": 1702000000,
    "expires_at": 1702000900
  }
}

Join Existing Kindling Group

Request
bash
curl -X POST https://api.sparkvault.com/v1/sparks \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "retry data",
    "ttl_minutes": 15,
    "kindling": "kdl_xyz789..."
  }'
Response
json
{
  "data": {
    "spark_id": "spk_def456...",
    "kindling": "kdl_xyz789...",
    "status": "active",
    "created_at": 1702000100,
    "expires_at": 1702001000
  }
}

Query Sparks by Kindling

Request
bash
curl "https://api.sparkvault.com/v1/sparks?kindling=kdl_xyz789" \
  -H "X-API-Key: sv_live_xxx"
Response
json
{
  "data": {
    "sparks": [
      {
        "spark_id": "spk_abc123...",
        "kindling": "kdl_xyz789...",
        "status": "ash",
        "created_at": 1702000000
      },
      {
        "spark_id": "spk_def456...",
        "kindling": "kdl_xyz789...",
        "status": "active",
        "created_at": 1702000100
      }
    ],
    "count": 2,
    "has_more": false
  }
}
Kindling Is Cross-Account by Design

Kindling enables cross-account sharing (SparkSync): possession of the 128-bit kindling ID is the authorization. Any authenticated caller who knows the ID of a kindling-scoped Spark can read it via GET /v1/sparks/{spark_id}, and ?kindling= list queries are not filtered by account. Treat kindling IDs like secrets. Only Sparks without a kindling are strictly scoped to the owning account.

Sharing Sparks

Share sparks via x.sv URLs using the /v1/sparks/:id/share endpoint. SparkLinks provide shareable URLs with visibility controls.

PATCH /v1/sparks/{spark_id}/share

Share a spark and get a public x.sv URL.

Request Body

ParameterTypeRequiredDescription
visibility string Optional Visibility mode: public, authenticated, or invite_onlyDefault: public
invites string[] Optional For invite_only visibility: invited identities (max 1)Default: []
expires_in_seconds integer Optional Link expiration TTL in seconds (min 60). The link expiry is capped at the Spark's own expires_at. Applied only when first sharing a Spark; on re-share (updating an existing link) it is ignored and the original link expiry is retained.Default: the Spark's own expiry
Share Preconditions

Only active, non-expired Sparks can be shared. Burned or expired Sparks return 403 FORBIDDEN; a nonexistent (or non-owned) spark_id returns 404 NOT_FOUND. Re-sharing an already-shared Spark returns the existing link with already_shared: true.

Response

FieldTypeDescription
shared boolean true: the Spark is shared
share_url string Shareable https://x.sv/{code} URL
link_code string SparkLink code for the URL
visibility string Current visibility setting
invites array? Invited identities (invite_only visibility)
expires_at integer? Unix timestamp when the link expires (never after the Spark itself)
created_at integer Unix timestamp the link was created
already_shared boolean? Present (true) when the Spark was already shared and the existing link was returned

Share a Spark

Request
bash
curl -X PATCH https://api.sparkvault.com/v1/sparks/spk_xxx/share \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "visibility": "public" }'
Response
json
{
  "data": {
    "shared": true,
    "share_url": "https://x.sv/abc123...",
    "link_code": "abc123...",
    "visibility": "public",
    "invites": [],
    "expires_at": 1702086400,
    "created_at": 1702000000
  }
}
GET /v1/sparks/{spark_id}/share

Get the sharing status for a spark.

Response

FieldTypeDescription
shared boolean Whether the Spark is currently shared. When false, only spark_id accompanies it.
spark_id string Spark identifier
share_url string? Shareable https://x.sv/{code} URL (when shared)
link_code string? SparkLink code (when shared)
visibility string? Visibility setting (when shared)
invites array? Invited identities (when shared)
status string? Single-use link lifecycle: activeconsumed | revoked
expires_at integer? Unix timestamp when the link expires (when shared)
created_at integer? Unix timestamp the link was created (when shared)
DELETE /v1/sparks/{spark_id}/share

Unshare a spark, deleting its SparkLink. The Spark itself remains intact.

Response

FieldTypeDescription
shared boolean false: the Spark is no longer shared
message string SparkLink deleted successfully, or Spark was not shared if there was no link (idempotent)
GET /v1/sparks/shared/{spark_id}/meta

Get metadata for a spark WITHOUT burning it. Authenticated owners can always check their own sparks; unauthenticated access works when the spark has a SparkLink.

Response

FieldTypeDescription
spark_id string Spark identifier
status string Always active (non-active Sparks are not viewable)
expires_at integer Unix timestamp when the Spark expires
time_remaining integer Seconds until expiration
has_filename boolean Whether the Spark carries a filename (file upload)
content_type string MIME type of payload
Anti-Enumeration

Unauthenticated callers receive a single opaque 404 NOT_FOUND for every non-viewable case (nonexistent, unshared, burned, or expired), so the endpoint cannot be used to enumerate Spark IDs. Authenticated owners get granular reasons.

Burn on Read

Shared sparks still burn when read. Once the recipient views the spark via the x.sv URL, the secret is destroyed.

Common Use Cases

Share Database Credentials

javascript
// Create a Spark with database credentials
const response = await fetch('https://api.sparkvault.com/v1/sparks', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.SPARKVAULT_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    payload: JSON.stringify({
      host: 'db.example.com',
      username: 'admin',
      password: 'super_secret_password'
    }),
    ttl_minutes: 60 // Expires in 1 hour
  })
});

const { spark_id } = (await response.json()).data;

// Mint a burn-on-read x.sv link for the recipient
const shareResponse = await fetch(`https://api.sparkvault.com/v1/sparks/${spark_id}/share`, {
  method: 'PATCH',
  headers: {
    'X-API-Key': process.env.SPARKVAULT_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ visibility: 'public' })
});

const { share_url } = (await shareResponse.json()).data;

// Hand out share_url via Slack, email, etc.
console.log(`Share this link: ${share_url}`);

Share Binary Files (PDF, Images)

javascript
// Read file and convert to base64
const fs = require('fs');
const fileBuffer = fs.readFileSync('contract.pdf');
const base64Content = fileBuffer.toString('base64');

const response = await fetch('https://api.sparkvault.com/v1/sparks', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.SPARKVAULT_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    filename: 'Signed Contract.pdf',
    payload: base64Content,
    content_type: 'application/pdf',  // Binary content
    ttl_minutes: 1440
  })
});

// Recipient reads the Spark
const readResponse = await fetch(`https://api.sparkvault.com/v1/sparks/${spark_id}`, {
  headers: { 'X-API-Key': recipientApiKey }
});
const { payload, content_type } = (await readResponse.json()).data;

// The server treats text/*, application/json, application/xml,
// application/javascript, and +json/+xml types as UTF-8 text;
// every other content type arrives base64-encoded.
const isText = content_type.startsWith('text/')
  || ['application/json', 'application/xml', 'application/javascript'].includes(content_type)
  || content_type.endsWith('+json')
  || content_type.endsWith('+xml');

if (!isText) {
  const buffer = Buffer.from(payload, 'base64');
  fs.writeFileSync('downloaded.pdf', buffer);
}

Secure CI/CD Secret Injection

bash In your deployment script
# Read the Spark and inject into environment
CREDENTIALS=$(curl -s "https://api.sparkvault.com/v1/sparks/$SPARK_ID" \
  -H "X-API-Key: $SPARKVAULT_API_KEY" | jq -r '.data.payload')

# Parse and export
export DB_PASSWORD=$(echo $CREDENTIALS | jq -r '.password')

# The Spark is now burned and cannot be read again
# Even if someone obtains the SPARK_ID, the secret is gone

API Key Rotation

python
import requests
import os

# Generate new API key and share via Spark
new_api_key = generate_new_api_key()

response = requests.post(
    'https://api.sparkvault.com/v1/sparks',
    headers={
        'X-API-Key': os.environ['SPARKVAULT_API_KEY'],
        'Content-Type': 'application/json'
    },
    json={
        'payload': new_api_key,
        'ttl_minutes': 1440  # 24 hours to claim
    }
)

spark_id = response.json()['data']['spark_id']
notify_team(f"New API key ready: {spark_id}")

Usage

Spark operations are covered by your seat subscription. Usage is tracked against the pool included with your plan; there are no per-operation charges. For subscription tiers and capacity blocks, see the pricing page.

Operation Usage
Create Spark Included with subscription
Read Spark (burn) Included with subscription
List Sparks Included with subscription
Delete Spark Included with subscription

Security Model

Double Zero-Trust Encryption

Sparks use two independent encryption keys:

  1. SparkVault Master Key (SVMK): A post-quantum ML-KEM-1024 keypair retrieved at runtime from AWS SSM Parameter Store. Sealing a Spark performs a software ML-KEM-1024 encapsulation to produce a per-Spark shared secret.
  2. Account Master Key (AMK): Your account's unique HMAC key managed in a FIPS 140-3 validated key management system (AWS KMS). The API calls KMS GenerateMac (HMAC-SHA512) to produce a per-Spark salt; the key itself never leaves KMS.

The per-Spark encryption key is derived with HKDF from the ML-KEM-1024 shared secret and the KMS HMAC salt, and the payload is sealed with AES-256-GCM. Both keys are required to decrypt a Spark. Even with full database access, SparkVault cannot decrypt Sparks without combining these keys, which are stored separately with strict access controls.

Atomic Burn

When a Spark is read, the decryption and deletion happen atomically behind a read lock. The encrypted data is deleted from storage before the plaintext is returned. This ensures that even in the event of a system failure, the Spark cannot be read twice: if the burn cannot be completed, the plaintext is withheld and the request fails with 500 SPARK_BURN_FAILED.

Error Reference

Error Responses

StatusCodeDescription
400 VALIDATION_ERROR Invalid request parameters (e.g., TTL out of range, payload over the 256 KB limit)
401 AUTHENTICATION_ERROR Invalid or missing API key
402 PLAN_REQUIRED An active subscription is required
403 FORBIDDEN Spark already burned or expired, or share preconditions not met
404 NOT_FOUND Spark does not exist
409 CONFLICT Spark is currently being read by another request
429 RATE_LIMIT_EXCEEDED Too many requests
500 SPARK_BURN_FAILED Burn failed after read: the plaintext is withheld and the request aborted