Overview
SparkLinks are the unified sharing mechanism for SparkVault. They provide a single, consistent way to share sparks and ingots with configurable visibility, single-use access, and expiration. Every SparkLink works exactly once, then expires.
Key Features
- Unified Sharing: One system for sparks and ingots
- Visibility Controls: Public, authenticated, or invite-only access
- Single-Use: Every link opens exactly once, then is consumed
- Expiration: Automatic link expiration
- 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 vault files | PUT /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing |
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
SparkLinks always use the short domain format x.sv/{link_code}.
List SparkLinks
/v1/sparklinks
List all SparkLinks created by your account. Supports filtering by type and pagination.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | Optional | Filter by link type: spark or ingotDefault: all |
limit |
integer | Optional | Maximum results to return (1-100)Default: 50 |
cursor |
string | Optional | Pagination cursor from previous responseDefault: null |
Response
| Field | Type | Description |
|---|---|---|
sparklinks |
array | Array of SparkLink objects |
sparklinks[].link_code |
string | Unique link code |
sparklinks[].link_url |
string | Full URL: https://x.sv/{link_code} |
sparklinks[].link_type |
string | Type: spark or ingot |
sparklinks[].asset_id |
string? | Associated asset ID (spk_xxx or ing_xxx) |
sparklinks[].vault_id |
string? | Vault ID for ingot links |
sparklinks[].visibility |
string | Visibility mode |
sparklinks[].invites |
array | Invited identities (email/phone strings) |
sparklinks[].status |
string | Single-use lifecycle: active → consumed (opened once) | revoked |
sparklinks[].bound_ip |
string? | IP address the link is bound to |
sparklinks[].session_length |
integer? | Access session length in seconds |
sparklinks[].expires_at |
integer? | Expiration timestamp |
sparklinks[].created_at |
integer | Creation timestamp |
sparklinks[].verified_by |
string? | Last verified identity (for authenticated/invite_only) |
sparklinks[].verified_at |
integer? | Last verification timestamp |
count |
integer | Number of items in this page |
cursor |
string? | Pagination cursor for the next page (null when there are no more pages) |
List All SparkLinks
Requestcurl "https://api.sparkvault.com/v1/sparklinks?type=spark&limit=10" \
-H "X-API-Key: sv_live_xxx"
{
"data": {
"sparklinks": [
{
"link_code": "ABCDefgh1234567890_-xy",
"link_url": "https://x.sv/ABCDefgh1234567890_-xy",
"link_type": "spark",
"asset_id": "spk_abc123",
"visibility": "public",
"invites": [],
"status": "active",
"expires_at": 1702086400,
"created_at": 1702000000
}
],
"count": 1,
"cursor": null
}
}
Create a Redirect / Magic Link (Link-Spark)
A single-use redirect or magic link is a link-spark: a Spark whose encrypted payload is the destination URL, marked by content_type: 'text/uri-list'. The destination is sealed at rest (never stored in plaintext) and burns on first open. Create it on the Sparks API: the server validates the URL, seals it, and auto-mints a PUBLIC spark SparkLink grant, returning link_url and link_code. There is no POST /v1/sparklinks create path.
/v1/sparks
Create a link-spark by sealing a destination URL. Pass content_type 'text/uri-list' and the URL as the payload.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
payload |
string | Required | The http/https destination URL to seal |
content_type |
string | Required | Must be 'text/uri-list' to mint a link-spark |
ttl_minutes |
integer | Optional | Time-to-live in minutesDefault: 1440 (24 hours) |
Response
| Field | Type | Description |
|---|---|---|
spark_id |
string | The sealed link-spark (spk_xxx) |
size_bytes |
integer | Size of the sealed payload in bytes |
status |
string | The spark's status: active |
created_at |
integer | Creation timestamp |
expires_at |
integer | Expiration timestamp |
ttl_minutes |
integer | Time-to-live in minutes |
link_url |
string | Full URL: https://x.sv/{link_code} |
link_code |
string | Unique link code |
filename |
string? | Echoed back when provided in the request |
kindling |
string? | External kindling ID, when provided in the request |
Create Magic Link (Link-Spark)
Requestcurl -X POST https://api.sparkvault.com/v1/sparks \
-H "X-API-Key: sv_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"payload": "https://myapp.com/auth/callback?token=xyz",
"content_type": "text/uri-list",
"ttl_minutes": 15
}'
{
"data": {
"spark_id": "spk_abc123",
"size_bytes": 41,
"status": "active",
"created_at": 1702000000,
"expires_at": 1702000900,
"ttl_minutes": 15,
"link_url": "https://x.sv/ABCDefgh1234567890_-xy",
"link_code": "ABCDefgh1234567890_-xy"
}
}
Opening x.sv/{link_code} serves a "Decrypting the secure SparkLink destination…" page without burning. Page JS then posts to burn: the server decrypts the URL, runs a link-safety check, and returns it, and the page redirects to the destination host. A non-JS scanner that issues only a bare GET never consumes the single-use link. If the URL is flagged unsafe, the page shows a warning instead of redirecting.
Get SparkLink
/v1/sparklinks/{link_code}
Get details about a specific SparkLink.
Response
| Field | Type | Description |
|---|---|---|
link_code |
string | Unique link code |
link_url |
string | Full URL: https://x.sv/{link_code} |
link_type |
string | Type: spark or ingot |
asset_id |
string? | Associated asset ID (spk_xxx or ing_xxx) |
vault_id |
string? | Vault ID for ingot links |
visibility |
string | Visibility mode |
invites |
array | Invite objects { identity, type } where type is email or phone (invite_only visibility) |
status |
string | Single-use lifecycle: active → consumed (opened once) | revoked |
bound_ip |
string? | IP address the link is bound to |
session_length |
integer? | Access session length in seconds |
expires_at |
integer? | Expiration timestamp |
created_at |
integer | Creation timestamp |
verified_by |
string? | Last verified identity (for authenticated/invite_only) |
verified_at |
integer? | Last verification timestamp |
Get SparkLink Details
Requestcurl "https://api.sparkvault.com/v1/sparklinks/ABCDefgh1234567890_-xy" \
-H "X-API-Key: sv_live_xxx"
{
"data": {
"link_code": "ABCDefgh1234567890_-xy",
"link_url": "https://x.sv/ABCDefgh1234567890_-xy",
"link_type": "spark",
"asset_id": "spk_abc123",
"visibility": "public",
"invites": [],
"status": "active",
"expires_at": 1702086400,
"created_at": 1702000000
}
}
An unknown link code, or one owned by another account, returns 400 VALIDATION_ERROR with message SparkLink not found, not a 404.
Delete SparkLink
/v1/sparklinks/{link_code}
Delete a SparkLink. The associated asset (spark/ingot) will no longer be shared, but the asset itself is not deleted.
Response
| Field | Type | Description |
|---|---|---|
deleted |
boolean | Always true on success |
link_code |
string | The deleted link code |
Delete SparkLink
Requestcurl -X DELETE "https://api.sparkvault.com/v1/sparklinks/ABCDefgh1234567890_-xy" \
-H "X-API-Key: sv_live_xxx"
{
"data": {
"deleted": true,
"link_code": "ABCDefgh1234567890_-xy"
}
}
Each spark or ingot can have at most one SparkLink. Deleting a SparkLink makes the asset private again. To change a shared spark's settings, PATCH /v1/sparks/{spark_id}/share again, which updates the existing link in place.
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.
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
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
| Field | Type | Description |
|---|---|---|
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)
Requestcurl -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"
}'
{
"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)
Requestcurl -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"]
}'
{
"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
/v1/sparks/{spark_id}/share
Get the current sharing status for a spark.
Response
| Field | Type | Description |
|---|---|---|
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
/v1/sparks/{spark_id}/share
Unshare a spark by deleting its SparkLink. The spark itself is not deleted.
Response
| Field | Type | Description |
|---|---|---|
shared |
boolean | Always false after unsharing |
message |
string | Result message (also returned when the spark was not shared) |
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 via SparkLinks. Every SparkLink grant is single-use regardless of type: it is minted active, atomically flipped to consumed on the one open, and then rejects all further access. Re-access requires minting a new link.
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
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
| Field | Type | Description |
|---|---|---|
ingot_id |
string | Ingot ID |
shared |
boolean | Whether the ingot is shared |
visibility |
string? | Visibility mode (null when not shared) |
public_url |
string? | Full share URL: https://x.sv/{link_code} |
link_code |
string? | Unique link code |
updated_at |
integer | Update timestamp |
Share Ingot
Requestcurl -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"
}'
{
"data": {
"ingot_id": "ing_xyz789",
"shared": true,
"visibility": "authenticated",
"public_url": "https://x.sv/ABCDefgh1234567890_-xy",
"link_code": "ABCDefgh1234567890_-xy",
"updated_at": 1702000000
}
}
GET /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing: Get the current sharing configurationPOST /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing/invite: Invite an identity (body:identity, optionalexpires_in_seconds)DELETE /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing/invite/{invite_id}: Revoke an invitePOST /v1/vaults/{vault_id}/ingots/{ingot_id}/sharing/invite/{invite_id}/resend: Resend an invitation
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. SparkLinks are always served from x.sv, so there is no link-hosting step here.
Listing domains works with any authenticated caller (JWT or API key). Adding, deleting, and verifying domains are admin-only (JWT admin session).
/v1/domains
List the account's custom domains and their ownership-verification status.
Response
| Field | Type | Description |
|---|---|---|
domains |
array | Array of domain objects |
domains[].domain |
string | The hostname |
domains[].status |
string | Ownership verification: pending | verified | failed |
domains[].verification_method |
string | Always dns_txt |
domains[].txt_name |
string | TXT record name to create (_sparkvault.{domain}) |
domains[].txt_value |
string | TXT record value (sparkvault-domain-verification=svdv_...) |
domains[].created_at |
integer | Creation timestamp |
domains[].verified_at |
integer? | Ownership verification timestamp |
domains[].last_checked_at |
integer? | Last TXT check timestamp |
domains[].last_check_result |
string? | Last TXT check outcome |
/v1/domains
Add a custom domain (admin only). The domain starts pending. Prove ownership via the verify endpoint. Maximum 10 domains per account.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
domain |
string | Required | Hostname to add (e.g. app.example.com). SparkVault-owned domains are rejected. |
Response
| Field | Type | Description |
|---|---|---|
domain |
object | The created domain object (see GET /v1/domains fields) |
message |
string | Setup instructions |
Add a Custom Domain
Requestcurl -X POST https://api.sparkvault.com/v1/domains \
-H "Authorization: Bearer ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{ "domain": "app.acme.com" }'
{
"data": {
"domain": {
"domain": "app.acme.com",
"status": "pending",
"verification_method": "dns_txt",
"txt_name": "_sparkvault.app.acme.com",
"txt_value": "sparkvault-domain-verification=svdv_...",
"created_at": 1702000000,
"verified_at": null,
"last_checked_at": null,
"last_check_result": null
},
"message": "Domain added. Create the TXT record to verify ownership."
}
}
/v1/domains/{domain}
Remove a custom domain (admin only) and release its global claim.
Response
| Field | Type | Description |
|---|---|---|
deleted |
boolean | Always true on success |
domain |
string | The removed hostname |
/v1/domains/{domain}/verify
Verify DNS ownership via a TXT record (admin only). Rate limited: one attempt per domain every 10 minutes (429 RATE_LIMITED with retry_after).
Response
| Field | Type | Description |
|---|---|---|
domain |
object | The updated domain object |
verified |
boolean | Whether ownership verification passed |
result |
string | TXT ownership check outcome (e.g. success, txt_not_found, txt_wrong_value) |
message |
string | Human-readable result with the next step |
actual_value |
string? | TXT value found, when it differs from the expected value |
Verify a Custom Domain
Requestcurl -X POST https://api.sparkvault.com/v1/domains/app.acme.com/verify \
-H "Authorization: Bearer ADMIN_JWT"
{
"data": {
"domain": {
"domain": "app.acme.com",
"status": "verified",
"verified_at": 1702000600
},
"verified": true,
"result": "success",
"message": "Domain verified successfully"
}
}
Successful verification claims the domain globally: a domain verified by one organization cannot be verified by another (the attempt fails with 400 VALIDATION_ERROR). A verified domain authorizes cross-domain SSO and Identity SDK browser origins for the claiming organization.
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
Requestcurl "https://api.sparkvault.com/v1/audit-logs?event_types=sparklink_accessed" \
-H "X-API-Key: sv_live_xxx"
{
"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
}
}
sparklink_created: SparkLink was createdsparklink_updated: SparkLink settings were updated in placesparklink_accessed: SparkLink was accessed (spark / link-spark / ingot)sparklink_deleted: SparkLink was deletedsparklink_revoked: SparkLink was revokedsparklink_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)
// 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
// 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
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
SparkLink operations are covered by your seat subscription. Bandwidth consumed by link access 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 SparkLink | Included with subscription |
| Access SparkLink | Counts against pooled bandwidth (included) |
| List SparkLinks | Included with subscription |
| Delete SparkLink | Included with subscription |
Error Reference
Error Responses
| Status | Code | Description |
|---|---|---|
| 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 |
| 402 | PLAN_REQUIRED |
An active subscription is required |
| 402 | QUOTA_EXCEEDED |
Pooled capacity exhausted (details include resource: bandwidth). Add a capacity block |
| 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) |
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.