Sparker Verify Documentation
Learn how to integrate hardware-backed media verification into your application. From quick setup to browser capture and public verification.
Overview
Sparker Verify prevents AI-generated, edited, or re-uploaded media from being passed off as authentic captures. It binds every image, video, or audio file to a hardware-backed private key on the user's physical device at the moment of capture, then wraps the cryptographic proof in a C2PA manifest that any relying party can verify independently.
Hardware-backed signing
ECDSA P-256 keys generated inside secure hardware. Today the SDK supports browser WebAuthn; native iOS and Android support is planned.
Nonce-challenge protocol
Every verification uses a server-generated nonce with 5-minute TTL to prevent replay attacks.
Public verification
Anyone can verify media authenticity using the public API or SDK. Files never leave the client.
Entities
| Entity | Role |
|---|---|
| Prover (Client Device) | Holds hardware-backed private key, signs file+nonce hash |
| Verifier (Sparker Backend) | Orchestrates challenges, validates signatures, mints C2PA manifests |
| Relying Party (Business) | Consumes verified media and checks authenticity via public API |
Authentication
Sparker uses Google OAuth for dashboard access and dashboard-managed API keys for SDK traffic. Initialize the SDK once with an API key and it will bind verification requests to that account. Public verification endpoints still work without authentication, but they are much more heavily rate-limited.
For delegated third-party capture, 3P verification link tokens let you share verification capability with people who don't have Sparker accounts. See the for details.
Quickstart
The SDK now has two fast paths: a verification overlay for published media and a capture component for live browser submissions.
1. Install the SDK
npm install @sparker-io/verify-sdk2. Wrap a published image
import { SparkerVerify } from '@sparker-io/verify-sdk'
import { SparkerVerifiedImage } from '@sparker-io/verify-sdk/react'
const client = new SparkerVerify({
apiUrl: 'https://verify.sparker.io',
appUrl: 'https://verify.sparker.io',
apiKey: 'spk_live_...',
})
export function StoryImage() {
return (
<SparkerVerifiedImage client={client}>
<img
src="/story/harbor-wall.jpg"
alt="Concrete crews reinforcing the harbor wall"
/>
</SparkerVerifiedImage>
)
}3. Drop in the capture desk
The React capture component handles first-run browser registration, nonce creation, signature generation, and upload.
import { SparkerCapture } from '@sparker-io/verify-sdk/react'
export function CaptureDesk() {
return (
<SparkerCapture
apiKey="spk_live_..."
apiUrl="https://verify.sparker.io"
appUrl="https://verify.sparker.io"
onVerified={(result) => {
console.log(result.id)
console.log(result.signature_hash)
console.log(result.verificationUrl)
}}
/>
)
}4. Use the low-level client when needed
const sdk = new SparkerVerify({
apiUrl: 'https://verify.sparker.io',
appUrl: 'https://verify.sparker.io',
apiKey: 'spk_live_...',
})
await sdk.registerBrowserDevice('Field Reporter Browser')
const verification = await sdk.submitCapturedFile(file, {
metadata: {
gps: {
latitude: 40.7128,
longitude: -74.006,
accuracy: 8,
},
},
})
console.log(verification.signature_hash)
console.log(
sdk.buildVerificationUrl(
verification.signature_hash,
undefined,
verification.id,
),
)- Read the concept to understand what happens under the hood
- Explore the API reference for all available endpoints
- Check the Next.js demo in
sdk/examples/nextjs-demo/for a newsroom-style integration example
Concepts
Understanding these core concepts will help you integrate Sparker Verify effectively and make informed architectural decisions.
C2PA standard
The Coalition for Content Provenance and Authenticity (C2PA) is an open technical standard that provides a way to trace the origin and history of digital content. Sparker Verify uses C2PA manifests to embed cryptographic provenance data directly into media files.
A C2PA manifest contains claims and assertions. Sparker creates a custom assertion called Hardware Verified Capture that records:
- The hardware attestation proof (device platform, attestation format)
- The nonce used during capture (single-use, 5-minute TTL)
- The device signature verification status (verified against stored public key)
- GPS coordinates at time of capture (if provided)
- Device information (name, model, platform)
The manifest is signed with the platform's X.509 certificate chain, making it independently verifiable by any C2PA-compatible tool.
Hardware attestation
Hardware attestation proves that a cryptographic key was generated and is stored inside a Trusted Execution Environment (TEE). This provides a strong guarantee that the signing key cannot be extracted, copied, or used by software alone.
| Platform | Status | Attestation format | Notes |
|---|---|---|---|
| Web | Supported now | webauthn | Browser capture and device registration through WebAuthn |
| iOS | Planned | Not supported yet | Native iOS SDK and Secure Enclave flow are not available yet |
| Android | Planned | Not supported yet | Native Android SDK and StrongBox / TEE flow are not available yet |
Supported device registrations currently use the browser WebAuthn flow and submit public keys in a unified format: {algorithm: "ES256", x: "<base64>", y: "<base64>"}. Native iOS and Android formats will follow the same server-side model once those SDKs ship.
Verification flow
The verification lifecycle has three phases: device registration, media capture with signing, and public verification.
Phase 1: Device registration (one-time)
Client Server
| |
|--- POST /devices/challenge ---------->|
|<-- { challenge } ---------------------|
| |
| [generate key pair in TEE] |
| [sign challenge with private key] |
| [collect attestation proof] |
| |
|--- POST /devices/register ----------->|
| { platform, device_id, |
| public_key, attestation } |
| |
| [verify challenge_response] |
| [verify attestation chain] |
| [store public_key + device_id] |
| |
|<-- { id, device_id, platform } -------|Phase 2: Capture and sign
Client Server
| |
|--- POST /verifications -------------->|
|<-- { id, nonce, expires_at } ---------|
| |
| [capture media live] |
| [compute SHA-256(file + nonce)] |
| [sign hash with hardware key] |
| |
|--- POST /verifications/verify ------->|
| { id, file, device_id, |
| signature, metadata } |
| |
| [verify nonce is valid + unused] |
| [lookup device by device_id] |
| [verify signature with public_key] |
| [create C2PA manifest] |
| [store file + register hash] |
| |
|<-- { authentic, |
| signature_hash } ---------------|Phase 3: Public verification
Relying parties verify media authenticity without uploading files. The signature hash (64-character hex string extracted from the C2PA manifest) is checked against Sparker's registry.
// Client-side verification (no file upload)
const result = await sdk.verifySignature('abc123def456...')
if (result.authentic) {
console.log('Verified at:', result.verified_at)
}Guides
Web integration
Integrate Sparker Verify into your web application using the JavaScript SDK. This guide covers the complete flow from setup to verification.
Prerequisites
- A Sparker account with API access
- A modern browser with WebAuthn support (Chrome 67+, Firefox 60+, Safari 14+)
- HTTPS enabled on your domain (required for WebAuthn)
Authentication setup
Generate an API key in the dashboard, then initialize the SDK with that key. OAuth still powers dashboard sessions, but the SDK itself should use the API key.
const sdk = new SparkerVerify({
apiUrl: 'https://verify.sparker.io',
appUrl: 'https://verify.sparker.io',
apiKey: 'spk_live_...',
})Checking WebAuthn support
import { isWebAuthnAvailable } from '@sparker-io/verify-sdk'
if (!isWebAuthnAvailable()) {
// Show fallback: WebAuthn not supported
showError('Your browser does not support hardware-backed signing.')
}Error handling
The SDK throws typed errors you can catch and handle appropriately.
import { SparkerError } from '@sparker-io/verify-sdk'
try {
const result = await sdk.submitCapturedFile(file)
} catch (error) {
if (error instanceof SparkerError && error.status === 429) {
// Rate limited - retry after delay
showRateLimitWarning(60)
} else if (error instanceof SparkerError && error.status === 400) {
// Bad request - check input
showError(error.message)
} else {
showError('Verification failed. Please try again.')
}
}Using the SDK
The SDK is intentionally component-first. Use the React layer when you want fast adoption, then drop down to the low-level client for custom UI or non-React flows.
Verification overlay
Wrap an existing image, let the SDK fetch the bytes, resolve the lookup hash, and show the compact green verification badge in the corner. When the backend returns a matched verification ID, the badge links directly to the frontend verification page for that record.
import { SparkerVerify } from '@sparker-io/verify-sdk'
import { SparkerVerifiedImage } from '@sparker-io/verify-sdk/react'
const client = new SparkerVerify({
apiUrl: 'https://verify.sparker.io',
appUrl: 'https://verify.sparker.io',
apiKey: 'spk_live_...',
})
<SparkerVerifiedImage client={client}>
<img src={imageUrl} alt="Field photograph" />
</SparkerVerifiedImage>Capture workflow
SparkerCapture handles registration, the nonce request, the browser signature step, and the upload to Sparker.
import { SparkerCapture } from '@sparker-io/verify-sdk/react'
<SparkerCapture
apiKey="spk_live_..."
apiUrl="https://verify.sparker.io"
appUrl="https://verify.sparker.io"
/>Low-level verification
const sdk = new SparkerVerify({
apiUrl: 'https://verify.sparker.io',
appUrl: 'https://verify.sparker.io',
apiKey: 'spk_live_...',
})
// Option 1: Verify by lookup hash
const lookupHash = 'abc123...'
const result = await sdk.verifySignature(lookupHash)
// Option 2: Verify a local file
const local = await sdk.verifyFile(file)
// Option 3: Verify a published image URL
const remote = await sdk.verifySource(imageUrl)
// Build the public verification URL
const url = sdk.buildVerificationUrl(
lookupHash,
imageUrl,
result.verification_id,
)3P verification links
3P verification links let authenticated users delegate capture to third parties who do not have Sparker accounts. Common use cases include insurance claims, journalism workflows, and field documentation.
Creating a 3P verification link
// Create a token with 10 uses, expiring in 7 days
const token = await sdk.createToken({
max_uses: 10,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
})
// Share the 3P verification link
console.log(token.url)
// https://verify.sparker.io/capture?token=...How 3P verification links work
- User opens the 3P verification link. No login or account creation required.
- The token is validated against the server. Invalid or expired tokens show an error.
- User captures media directly through the browser camera.
- The verification is associated with the token creator's account and quota.
- The token's usage count increments. When max uses are reached, the token expires.
Managing tokens
// List all your tokens
const { tokens } = await sdk.listTokens()
// Revoke a token
await sdk.revokeToken(tokenId)
// Check token validity (public, no auth required)
const status = await sdk.validateToken('token-string')
console.log(status.valid, status.remaining_uses)SDK reference
Complete reference for the core client and the React component layer.
Constructor
new SparkerVerify(config: {
apiUrl: string // Sparker API base URL
appUrl?: string // Public frontend base URL for verification links
apiKey?: string // Dashboard-generated API key for SDK traffic
timeout?: number // Request timeout in ms (default: 30000)
})Authentication
| Method | Description |
|---|---|
setApiKey(key) | Set or rotate the API key used for SDK requests |
setAnonymousToken(token) | Use a 3P verification link token instead of the account key (legacy helper name) |
clearApiKey() | Remove the configured API key |
React exports
| Method | Description |
|---|---|
SparkerVerifiedImage | Wrap an image and render the hover-to-expand badge |
SparkerCapture | Browser capture flow with registration, signing, and upload |
@sparker-io/verify-sdk/react | React-only entry point for the component layer |
Browser capture helpers
| Method | Description |
|---|---|
registerBrowserDevice(name?) | Create and persist the browser WebAuthn credential |
getStoredBrowserDevice() | Return the stored browser credential metadata |
clearStoredBrowserDevice() | Forget the stored browser credential reference |
submitCapturedFile(file, options) | Create nonce, sign, and upload a captured file in one call |
Verification methods (auth optional)
| Method | Description |
|---|---|
createVerification() | Create verification and receive a 5-minute nonce |
submitVerification(...) | Upload signed media with device_id, signature, and metadata |
getVerification(id) | Get verification status and details |
Public verification methods (no auth)
| Method | Description |
|---|---|
verifySignature(hash) | Check signature hash against registry |
verifyFile(file) | Resolve a local lookup hash, then verify |
verifySource(url) | Fetch a published asset and verify it client-side |
verifyBatch(hashes) | Verify up to 100 hashes at once |
getVerificationDetails(id) | Get public verification details for a public verification record |
buildVerificationUrl(hash, src?, verificationId?) | Build a public verification URL, preferring `/verify/:id` when a matched verification ID is available |
Platform helpers
| Function | Description |
|---|---|
computeSignedData(file, nonce) | Compute SHA-256(file + nonce) for signing |
extractWebAuthnPublicKey(response) | Extract P-256 x,y from WebAuthn attestation |
buildWebAuthnCreateOptions(challenge, userId, name) | Build WebAuthn registration options |
buildWebAuthnGetOptions(hash, credentialId) | Build WebAuthn assertion options |
isWebAuthnAvailable() | Check if WebAuthn is supported |
bufferToBase64Url(buffer) | ArrayBuffer to base64url string |
base64ToBuffer(base64) | Base64url string to ArrayBuffer |
sha256Hex(data) | SHA-256 hash as hex string |
Types
type VerificationLookupStrategy = "c2pa_signature" | "file_hash"
interface ResolvedPublicVerifyResponse {
authentic: boolean
verified_at: string
lookup_hash: string
lookup_strategy: VerificationLookupStrategy
}
interface VerificationVerifyResponse {
id: string
authentic: boolean
signature_hash: string
verified_at: string
device: {
name: string
model: string
platform: string
} | null
gps: GPSData | null
}
interface TokenValidateResponse {
valid: boolean
creator_name: string | null
remaining_uses: number | null
expires_at: string | null
}Platforms
Sparker Verify currently supports hardware-backed signing in web browsers through WebAuthn. Native iOS and Android SDKs are planned, but they are not supported yet.
Web (WebAuthn)
Web uses the WebAuthn/FIDO2 API to access platform authenticators like TouchID and Windows Hello. Keys are ECDSA P-256, generated in the browser's platform authenticator.
import {
buildWebAuthnCreateOptions,
extractWebAuthnPublicKey,
bufferToBase64Url,
} from '@sparker-io/verify-sdk'
// Registration
const options = buildWebAuthnCreateOptions(challenge, userId, userEmail)
const credential = await navigator.credentials.create({ publicKey: options })
const response = credential.response as AuthenticatorAttestationResponse
const publicKey = extractWebAuthnPublicKey(response)
// Signing
const assertion = await navigator.credentials.get({
publicKey: buildWebAuthnGetOptions(signedDataHash, credentialId),
})
const signature = bufferToBase64Url(
(assertion.response as AuthenticatorAssertionResponse).signature
)isWebAuthnAvailable() to check support before attempting registration.iOS (planned)
Native iOS device registration and capture are not supported yet. A future iOS SDK is expected to use Secure Enclave-backed keys, but there is no supported production flow today.
Android (planned)
Native Android device registration and capture are not supported yet. A future Android SDK is expected to target StrongBox or TEE-backed keys, but there is no supported production flow today.