SparkLinks API v1

Overview

SparkVault uses one grant system for sparks and ingots with configurable visibility and expiration. Spark and one-time ingot grants use x.sv and are consumed after one access. Durable ingot grants use files.sv and remain available until the owner disables sharing.

Key Features

  • Unified Sharing: One system for sparks and ingots
  • Visibility Controls: Public, authenticated, or invite-only access
  • Purpose-Built Lifecycles: Single-use links for sparks and one-time ingot delivery; durable links for ongoing ingot sharing
  • Expiration: Automatic expiration for single-use grants
  • Audit Trail: Track who accessed what and when
  • Link Safety: A link-spark destination URL is checked for safety before redirect

Link Types

Type Purpose Created Via
spark Share ephemeral secrets, including a link-spark (a text/uri-list redirect / magic link), burn-on-read PATCH /v1/sparks/{spark_id}/share, or POST /v1/sparks with content_type: 'text/uri-list' for a link-spark
ingot Share a vault file through a durable link or an independent one-time link PUT /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing (durable), or POST /v1/vaults/{vault_id}/ingots/{ingot_id}/sparklinks (one-time)

Visibility Modes

Mode Who Can Access Identity Verification
public Anyone with the link No
authenticated Anyone who verifies their identity Yes (via Identity Product)
invite_only Specific invited identities only Yes (must match invite)

URL Format

Sparks and one-time ingot links use x.sv/{link_code}. Durable ingot shares use files.sv/{link_code}.

Sharing Sparks

Share an existing spark by creating a SparkLink for it. The spark remains burn-on-read but can be accessed via the short URL.

PATCH /v1/sparks/{spark_id}/share

Create a SparkLink for an existing spark. If the spark is already shared, its existing link is updated in place.

Request Body

ParameterTypeRequiredDescription
visibility string Optional Visibility mode: public, authenticated, or invite_onlyDefault: public
invites array Optional Identities (email/phone) to invite for invite_only visibility (max 1, invitation sent automatically)Default: []
expires_in_seconds integer Optional Link expiration TTL (min 60, capped at spark expiration). Applies only on the first share; it is ignored when the spark already has a SparkLink.Default: Spark expiration

Response

FieldTypeDescription
shared boolean Whether the spark is shared
share_url string Full share URL: https://x.sv/{link_code}
link_code string Unique link code
visibility string Visibility mode
invites array Invited identities (invite_only visibility)
expires_at integer Link expiration timestamp
created_at integer Creation timestamp
already_shared boolean? true when the spark already had a SparkLink. The existing link is updated in place and returned, including invites and expires_at.

Share Spark (Public)

Request
bash
curl -X PATCH "https://api.sparkvault.com/v1/sparks/spk_abc123/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/ABCDefgh1234567890_-xy",
    "link_code": "ABCDefgh1234567890_-xy",
    "visibility": "public",
    "invites": [],
    "expires_at": 1702086400,
    "created_at": 1702000000
  }
}

Share Spark (Invite Only)

Request
bash
curl -X PATCH "https://api.sparkvault.com/v1/sparks/spk_abc123/share" \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "visibility": "invite_only",
    "invites": ["recipient@example.com"]
  }'
Response
json
{
  "data": {
    "shared": true,
    "share_url": "https://x.sv/ABCDefgh1234567890_-xy",
    "link_code": "ABCDefgh1234567890_-xy",
    "visibility": "invite_only",
    "invites": ["recipient@example.com"],
    "expires_at": 1702086400,
    "created_at": 1702000000
  }
}

Get Sharing Status

GET /v1/sparks/{spark_id}/share

Get the current sharing status for a spark.

Response

FieldTypeDescription
shared boolean Whether the spark is shared. When false, only shared and spark_id are returned
spark_id string Spark ID
share_url string? Full share URL (when shared)
link_code string? Unique link code (when shared)
visibility string? Visibility mode (when shared)
invites array? Invited identities (when shared)
status string? Single-use lifecycle: active → consumed (opened once) | revoked
expires_at integer? Link expiration timestamp (when shared)
created_at integer? Creation timestamp (when shared)

Unshare a Spark

DELETE /v1/sparks/{spark_id}/share

Unshare a spark by deleting its SparkLink. The spark itself is not deleted.

Response

FieldTypeDescription
shared boolean Always false after unsharing
message string Result message (also returned when the spark was not shared)
Automatic Invitation

When sharing with invite_only visibility, an invitation email/SMS is automatically sent to each invited identity with the pre-populated share URL.

Sharing Ingots

Share vault files (ingots) with external users through one permanent files.sv URL. Public, authenticated, and invite-only policies update that durable URL in place. One-time SparkLinks are independent, expiring x.sv grants that are consumed when their decrypt stream is issued.

PUT /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing

Enable, update, or disable sharing for an ingot. Requires a Vault Access Token (X-Vault-Access-Token header) and Public File Sharing enabled on the vault.

Request Body

ParameterTypeRequiredDescription
shared boolean Optional true to enable sharing, false to disableDefault: Current state
visibility string Optional Visibility mode: public, authenticated, or invite_only (required when enabling sharing)Default: null
invites array Optional Identities (email/phone) to invite for invite_only visibilityDefault: []

Response

FieldTypeDescription
ingot_id string Ingot ID
shared boolean Whether the ingot is shared
visibility string? Visibility mode (null when not shared)
public_url string? Permanent share URL: https://files.sv/{link_code}
link_code string? Unique link code
updated_at integer Update timestamp

Share Ingot

Request
bash
curl -X PUT "https://api.sparkvault.com/v1/vaults/vlt_abc123/ingots/ing_xyz789/sharing" \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: YOUR_VAT" \
  -H "Content-Type: application/json" \
  -d '{
    "shared": true,
    "visibility": "authenticated"
  }'
Response
json
{
  "data": {
    "ingot_id": "ing_xyz789",
    "shared": true,
    "visibility": "authenticated",
    "public_url": "https://files.sv/ABCDefgh1234567890_-xy",
    "link_code": "ABCDefgh1234567890_-xy",
    "updated_at": 1702000000
  }
}
Related Endpoints
  • GET /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing: Get the current sharing configuration
  • POST /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing/invite: Invite an identity to the durable share (body: identity)
  • DELETE /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing/invite/{invite_id}: Revoke an invite
  • POST /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing/invite/{invite_id}/resend: Resend an invitation
  • POST /v1/vaults/{vault_id}/ingots/{ingot_id}/sparklinks: Mint an independent one-time x.sv URL (body: expires_in_seconds, 300–86400)
  • POST /v1/vaults/{vault_id}/ingots/{ingot_id}/sparklinks/{code}/deliver: Send an existing one-time URL to an email or E.164 phone (body: identity)

Custom Domains

Prove your organization owns a domain with a DNS TXT record. Verified domains authorize cross-domain SSO, Identity SDK browser origins, and signup reservation under that domain for the claiming organization. Shares remain on the platform-owned x.sv and files.sv domains, so there is no link-hosting step here.

A domain is only ever stored verified: request the TXT record from the challenge endpoint, publish it at your DNS host, then claim the domain — the claim runs the DNS check and stores the domain only when it passes. There is no pending or unverified state. Listing domains works with any authenticated caller (JWT or API key); the challenge, claim, and delete endpoints are admin-only (JWT admin session).

GET /v1/domains

List the account's verified domains.

Response

FieldTypeDescription
domains array Array of domain objects
domains[].domain string The hostname
domains[].verification_method string Always dns_txt
domains[].txt_name string TXT record name (_sparkvault.{domain}) — keep it published so ownership stays verifiable
domains[].txt_value string TXT record value (sparkvault-domain-verification=svdv_...)
domains[].created_at integer Claim timestamp
domains[].verified_at integer Ownership verification timestamp (equal to created_at — a domain is stored only once verified)
POST /v1/domains/challenge

Get the TXT record to publish for a domain (admin only). Stores nothing — the record is derived, so you can request it now and claim the domain whenever DNS has propagated. Rejections DNS cannot fix (a domain claimed by another organization, an invalid hostname, the 10-domain account limit) are raised here, before you touch your DNS host.

Request Body

ParameterTypeRequiredDescription
domain string Required Hostname to claim (e.g. app.example.com). SparkVault-owned domains are rejected.

Response

FieldTypeDescription
domain string The normalized hostname
txt_name string TXT record name to create (_sparkvault.{domain})
txt_value string TXT record value (sparkvault-domain-verification=svdv_...)

Get the TXT Record

Request
bash
curl -X POST https://api.sparkvault.com/v1/domains/challenge \
  -H "Authorization: Bearer ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "app.acme.com" }'
Response
json
{
  "data": {
    "domain": "app.acme.com",
    "txt_name": "_sparkvault.app.acme.com",
    "txt_value": "sparkvault-domain-verification=svdv_..."
  }
}
POST /v1/domains

Claim a domain (admin only). Runs the DNS TXT check and stores the domain only if it passes — a failed check stores nothing, and the error names the exact record to fix. Maximum 10 domains per account.

Request Body

ParameterTypeRequiredDescription
domain string Required Hostname whose TXT record (from the challenge endpoint) is already published.

Response

FieldTypeDescription
domain object The stored, verified domain object (see GET /v1/domains fields)
message string Confirmation

Verify & Claim a Domain

Request
bash
curl -X POST https://api.sparkvault.com/v1/domains \
  -H "Authorization: Bearer ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "app.acme.com" }'
Response
json
{
  "data": {
    "domain": {
      "domain": "app.acme.com",
      "verification_method": "dns_txt",
      "txt_name": "_sparkvault.app.acme.com",
      "txt_value": "sparkvault-domain-verification=svdv_...",
      "created_at": 1702000600,
      "verified_at": 1702000600
    },
    "message": "app.acme.com is verified and ready to use."
  }
}
DELETE /v1/domains/{domain}

Remove a domain (admin only) and release its global claim.

Response

FieldTypeDescription
deleted boolean Always true on success
domain string The removed hostname
Global Domain Claims

A successful claim reserves the domain globally: a domain verified by one organization cannot be claimed by another (the attempt fails with 400 VALIDATION_ERROR). Keep the TXT record published — it is the standing proof of ownership behind cross-domain SSO and Identity SDK browser origins.

Access History

Track who accessed your SparkLinks using the Audit Log API. SparkLink access events include IP address, user agent, and verified identity (for authenticated/invite_only links). Responses are paginated: entries are returned under entries with count, has_more, and (when more pages exist) next_cursor, and each response includes an event_types array listing every valid event type for the scope. Filter with the comma-separated event_types query parameter.

Query SparkLink Access Events

Request
bash
curl "https://api.sparkvault.com/v1/audit-logs?event_types=sparklink_accessed" \
  -H "X-API-Key: sv_live_xxx"
Response
json
{
  "data": {
    "entries": [
      {
        "event_id": "evt_abc123",
        "event_type": "sparklink_accessed",
        "timestamp": 1702001000,
        "details": {
          "link_code": "ABCDefgh1234567890_-xy",
          "link_type": "ingot",
          "visibility": "authenticated",
          "verified_by": "user@example.com",
          "ip": "192.168.1.1",
          "user_agent": "Mozilla/5.0..."
        }
      }
    ],
    "count": 1,
    "has_more": false
  }
}
Event Types
  • sparklink_created: SparkLink was created
  • sparklink_updated: SparkLink settings were updated in place
  • sparklink_accessed: SparkLink was accessed (spark / link-spark / ingot)
  • sparklink_deleted: SparkLink was deleted
  • sparklink_revoked: SparkLink was revoked
  • sparklink_signed, sparklink_approved, sparklink_denied, sparklink_replied: interaction receipts, signed, JWKS-verifiable proof of the recipient's action

Common Use Cases

Passwordless Authentication (Magic Links)

javascript
// Create a magic auth link as a link-spark: the destination URL is
// sealed (encrypted at rest) and burns on the first open.
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: `https://myapp.com/auth/verify?token=${authToken}`,
    content_type: 'text/uri-list',
    ttl_minutes: 15  // single-use, burns on first hop
  })
});

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

// Send link_url to user via email
await sendEmail(user.email, `Login here: ${link_url}`);

Secure File Sharing with Identity Verification

javascript
// Share a confidential document requiring identity verification
const response = await fetch(
  'https://api.sparkvault.com/v1/vaults/vlt_abc/ingots/ing_xyz/sharing',
  {
    method: 'PUT',
    headers: {
      'X-API-Key': process.env.SPARKVAULT_API_KEY,
      'X-Vault-Access-Token': vaultAccessToken,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      shared: true,
      visibility: 'invite_only',
      invites: ['client@example.com']
    })
  }
);

// Recipient automatically receives invitation email
// They must verify their email before accessing the file

Time-Limited Share Links

python
import requests
import os

# Share a spark with public visibility that expires in 1 hour
response = requests.patch(
    f'https://api.sparkvault.com/v1/sparks/{spark_id}/share',
    headers={
        'X-API-Key': os.environ['SPARKVAULT_API_KEY'],
        'Content-Type': 'application/json'
    },
    json={
        'visibility': 'public',
        'expires_in_seconds': 3600
    }
)

share_url = response.json()['data']['share_url']
print(f"Share this link: {share_url}")  # Valid for 1 hour (capped at spark expiration)

Usage

A SparkLink backed by a Spark is free, like the Spark itself. A SparkLink backed by an ingot draws the bandwidth pool included with your plan when the file is downloaded. There are no per-operation charges either way. For subscription tiers and capacity blocks, see the pricing page.

Operation Usage
Create SparkLink Free on every account
Access SparkLink Counts against pooled bandwidth (included)
List SparkLinks Free on every account
Delete SparkLink Free on every account

Error Reference

Error Responses

StatusCodeDescription
400 VALIDATION_ERROR Invalid request parameters. Includes malformed or non-http(s) link-spark destination URLs, and unknown or unowned link codes on GET /v1/sparklinks/{link_code} (message: SparkLink not found)
401 AUTHENTICATION_ERROR Invalid or missing API key
403 FORBIDDEN Denied at link open: visibility requirements not met, link already consumed, revoked, expired, or IP-binding mismatch. Also returned when sharing a non-active or expired spark
404 NOT_FOUND Spark or ingot not found on the share endpoints; unknown or expired link code at public access (SparkLink not found or expired)
412 PRECONDITION_FAILED Public File Sharing is not enabled on the vault when enabling ingot sharing (enable it on the vault first)
429 RATE_LIMIT_EXCEEDED Too many requests
429 RATE_LIMITED Domain verification attempted again within 10 minutes (details include retry_after)
Re-Sharing Is Not an Error

Sharing a spark that already has a SparkLink does not conflict. The existing link is updated in place and returned with already_shared: true, and its link_code is never rotated. Changing visibility or invites applies to the same link, so switching an invite_only link to public downgrades access on the existing URL.