# SparkLinks API: SparkVault API Reference

> Create and manage shared links for sparks and ingots. SparkLinks provide secure, trackable, single-use access to your content with fine-grained visibility controls.

Canonical: https://sparkvault.com/api/docs/sparklinks/ · OpenAPI: https://sparkvault.com/openapi.yaml

## 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

#### `GET /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 `ingot`Default: `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

Request

```bash
curl "https://api.sparkvault.com/v1/sparklinks?type=spark&limit=10" \
  -H "X-API-Key: sv_live_xxx"
```

Response

```json
{
  "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.

#### `POST /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)

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://myapp.com/auth/callback?token=xyz",
    "content_type": "text/uri-list",
    "ttl_minutes": 15
  }'
```

Response

```json
{
  "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"
  }
}
```

> **Bot-Resistant Deferred Burn**
>
> 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

#### `GET /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

Request

```bash
curl "https://api.sparkvault.com/v1/sparklinks/ABCDefgh1234567890_-xy" \
  -H "X-API-Key: sv_live_xxx"
```

Response

```json
{
  "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
  }
}
```

> **Unknown Link Codes**
>
> An unknown link code, or one owned by another account, returns `400 VALIDATION_ERROR` with message `SparkLink not found`, not a 404.

## Delete SparkLink

#### `DELETE /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

Request

```bash
curl -X DELETE "https://api.sparkvault.com/v1/sparklinks/ABCDefgh1234567890_-xy" \
  -H "X-API-Key: sv_live_xxx"
```

Response

```json
{
  "data": {
    "deleted": true,
    "link_code": "ABCDefgh1234567890_-xy"
  }
}
```

> **One SparkLink Per Asset**
>
> 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.

#### `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

| 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)

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

| 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

#### `DELETE /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) |

> **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 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.

#### `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

| 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

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://x.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 (body: `identity`, optional `expires_in_seconds`)
> -   `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

## 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).

#### `GET /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 |

#### `POST /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

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",
      "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."
  }
}
```

#### `DELETE /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 |

#### `POST /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

Request

```bash
curl -X POST https://api.sparkvault.com/v1/domains/app.acme.com/verify \
  -H "Authorization: Bearer ADMIN_JWT"
```

Response

```json
{
  "data": {
    "domain": {
      "domain": "app.acme.com",
      "status": "verified",
      "verified_at": 1702000600
    },
    "verified": true,
    "result": "success",
    "message": "Domain verified successfully"
  }
}
```

> **Global Domain Claims**
>
> 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

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

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](/pricing/).

| 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`) |

> **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.
