Documentation

Everything you need to use Lantern and integrate with the API.

Last updated: 2026-05-23

Getting Started

Lantern is a provenance system for digital art. Artists register their work on the Base blockchain, creating a permanent, tamper-proof record of ownership. Anyone can verify who created a piece using the website or browser extension.

  1. Create an account on the Dashboard (requires an invite key)
  2. Link your platform accounts to prove you own them
  3. Upload and register your art
  4. Share your Lantern ID or let the extension detect it automatically
  5. Manage authorized apps anytime at Connected Apps

Register Art

Registration creates a blockchain record tied to your identity.

  1. Go to the Dashboard and sign in
  2. Upload your image file (PNG, JPEG, WebP, GIF, or TIFF, up to 50MB)
  3. Provide the URL where the work is posted (must match a verified platform account)
  4. Lantern computes a SHA-256 hash, perceptual hashes (dHash + pHash), and three neural fingerprints (SSCD, CLIP, DINOv3) used for similarity matching
  5. The SHA-256 hash is registered on the Base blockchain with an EIP-712 signature
  6. You receive a Lantern ID (format: LNTN-xxxxxxxx) that anyone can look up

If a visually similar image is already registered, the system will block the registration and point you to the existing record. If that record is not yours, you can file a dispute.

Verify Content

There are three ways to verify content:

By Lantern ID

Enter the ID on the Verify page or visit /verify/LNTN-xxxxxxxx directly.

By File Upload

Upload an image on the Verify page. The system first tries an exact SHA-256 match. If that fails, it computes a perceptual hash (dHash) and searches for visually similar registered works. This catches compressed, resized, or re-encoded versions.

By Extension

Right-click any image and select "Lantern" from the context menu. The extension uploads the image to the API for exact and perceptual matching, and the popup opens with the result.

Browser Extension

The Lantern extension works on 14 supported platforms. Five support full identity-verification flows (Twitter/X, Pixiv, DeviantArt, ArtStation, Instagram); nine support auto-detection only (Pinterest, Tumblr, Reddit, Flickr, Behance, Dribbble, Bluesky, Cara, VGen). It automatically detects Lantern IDs in posts and shows colored verification pins.

Features

  • Automatic badge detection on supported platforms
  • Right-click any image to verify (exact + perceptual matching)
  • Click a badge to see full provenance details
  • Auto-detect registered art on pages (optional, enable in settings)
  • Per-platform toggles and cache management in the popup Settings panel

Platform Linking

Linking your platform account proves you own it. This is required before you can register content from that platform.

  1. Go to Dashboard and click "Link a Platform"
  2. Select the platform and enter your username and profile URL
  3. You receive a verification code
  4. Post the code in a public post on that platform
  5. Submit the proof URL (link to the public post containing the code)
  6. An admin reviews and verifies the link

Profile bio verification is not yet supported. Use a public post.

Supported platforms: Twitter/X, Pixiv, DeviantArt, ArtStation, Instagram.

Disputes

If someone registered your work, you can file a dispute from the verification page. Provide your email, a description of your claim, and evidence URLs showing you are the original creator. An admin will review the evidence and resolve the dispute.

If a dispute is overturned (the ownership claim is rejected), the registration is flagged and the badge changes to "Disputed" across all platforms where the extension is active.

Badge States

  • Verified Creator: Creator has a verified platform link matching the content source
  • Unverified Creator: Registration exists but creator identity is self-declared
  • Disputed: A dispute has been filed against this registration
  • Revoked: The creator or an admin revoked the registration
  • Chain Review Needed: The on-chain record does not match the database record

API Overview

Base URL: https://api.lantern-us.com

All responses are JSON. Authenticated endpoints require a JWT token in the Authorization: Bearer <token> header. Tokens are obtained via the login or register endpoints.

Code-generate your client: Download the OpenAPI 3.0 spec and feed it to openapi-generator-cli for a typed client in any language. The spec covers both the user-facing API documented in this section and the partner Platform API below.

Public Endpoints

No authentication required.

GET/verify/{lantern_id}

Look up a registration by Lantern ID. Returns creator info, metadata, chain status, dispute status, and perceptual hashes if available.

GET/verify/hash/{content_hash}

Look up by SHA-256 hash. Accepts with or without sha256: prefix. Returns 400 for invalid format, 404 if not found.

POST/verify/image

Upload-based verification. Multipart image field. Tries exact SHA-256 match first, falls back to perceptual matching. Returns a DisplayBlock the extension and web app render verbatim.

GET/creator/{wallet_address}

List content registered by a wallet address. Supports offset and limit query params for pagination.

GET/config/platforms

Returns the hostname-to-platform map and display labels used by the extension to auto-detect supported sites. Cached 1h client-side.

GET/config/features

Returns 5 runtime feature flags: perceptual_fallback_enabled, show_confidence_percent, show_basescan_link, dispute_button_visible, badge_auto_scan_enabled. Cached 5 min client-side.

GET/api/v1/optout/check/{content_hash}

Public opt-out registry lookup. Returns whether a SHA-256 hash has been registered with opt_out_training=true. AI training pipelines use this as a Spawning-compatible opt-out signal.

POST/report/{lantern_id}

Submit a report. Body: {reason, evidence_url?, reporter_email?}.

GET/health

Returns {status: 'ok'} if the API is running.

Auth Endpoints

Sign in + session

POST/auth/register

Create account. Body: {email, password, display_name, invite_key}. Returns JWT token and user info.

POST/auth/login

Sign in. Body: {email, password}. Returns JWT token. Throttled: 5 attempts per 15 minutes per email.

GET/auth/me

Get current user info including content count, linked platforms, wallet address, and admin status. Requires auth.

POST/auth/revoke-my-tokens

Invalidate all of the current user's outstanding JWT tokens. Used by web logout so signing out actually ends every session, not just the tab. Returns 204.

Account discovery + helpers

GET/auth/password-rules

Returns the password policy (minimum length, character class rules) used by the frontend register form. Public; no auth required.

GET/auth/terms-version

Returns the current Terms of Service version string. Used by partners and the register form to record the version a user agreed to at the time of signing.

Account management

DELETE/auth/me

Permanently delete your account. Body: {content_disposition: 'preserve' | 'anonymize' | 'revoke_all'}. Requires recent reauth. Honors the disposition choice on your registered content.

GET/auth/me/export

DSAR data export. Returns a single JSON document with profile, linked platforms, content records, history events, sessions, terms-assent events, and disputes. Rate-limited 1/hr/user.

Google sign-in

POST/auth/google/callback

Complete a Google OAuth sign-in. Body: {credential: <Google ID token JWT>}. Returns a Lantern JWT for the matched (or auto-created) account.

POST/auth/google/link

Link a Google account to an existing email-password account. Requires auth. Body: {credential: <Google ID token JWT>}.

Connected apps

GET/auth/connected-apps

List active OAuth grants on your account (third-party apps you've authorized to act on your behalf). Returns client_id, app name, scopes granted, last-used timestamp.

DELETE/auth/connected-apps/{client_id}

Revoke a partner app's access. Invalidates outstanding access + refresh tokens for that client. Returns 204.

PATCH/auth/connected-apps/{client_id}

Narrow an existing grant's scopes. Body: {scopes: [...]}. Cannot widen scopes; the user would need to re-authorize via OAuth to add.

Content Endpoints

All require authentication.

Lifecycle

POST/register

Register new content. Multipart form: image file + source_platform + source_url + optional title, description, creation_tool. Requires a verified platform link. Limited to 10/day.

POST/revoke/{lantern_id}

Revoke your own registration. This is permanent and triggers an on-chain revocation.

POST/reactivate/{lantern_id}

Reactivate a self-revoked work. Available via the V2 contract. Only the original creator can reactivate.

POST/{lantern_id}/transfer

Transfer ownership of a work to another creator. Body: {new_creator_email, reason_category, terms_agreed_version}. Atomic with on-chain transferContentOwnership; old record is superseded by a new lantern_id under the buyer's wallet.

Backfill + signing

PATCH/{lantern_id}/backfill-hash

Add perceptual hashes to an existing registration. Upload the original file. Only the creator can backfill. Verifies SHA-256 matches.

POST/{lantern_id}/embed

Backfill neural embeddings (SSCD + CLIP + DINOv3) on an existing registration. Upload the original file; SHA-256 must match the registered hash. Enables matcher coverage on legacy works.

POST/{lantern_id}/c2pa-sign

Sign the content with Lantern's C2PA certificate. Returns a .c2pa manifest URL embeddable in the original file. Creator-only.

Recovery + introspection

POST/register/recover-orphan

Recover an on-chain registration whose off-chain database row is missing. Multipart form: image file + chain_tx. Verifies the SHA-256 matches the on-chain hash and rebuilds the row.

GET/{lantern_id}/training-set-check

Tier 1 membership-inference check: was this content's SHA-256 hash found in any known AI training dataset (LAION-5B, CC-12M, Common Crawl)? Returns {status, datasets_checked, matches}.

Stats

GET/stats

Admin-only. Platform aggregates: total images, creators, on-chain count, file type breakdown, contract address. Returns 403 for non-admin accounts.

Similarity Search

Perceptual + neural matching endpoints for detecting modified copies of registered art.

GET/verify/similar/{dhash_hex}?threshold=10

Search by 16-character hex dHash. Returns up to 5 matches within the Hamming distance threshold (0-20, default 10). Includes confidence scores and match type.

POST/verify/similar/batch

Batch search. Body: {hashes: [...], threshold: 10}. Up to 50 hashes per request. Returns matches grouped by query hash.

Match Types

  • neural_strong (SSCD/CLIP/DINOv3 fused distance < 0.15): Highest confidence; the matcher's primary path.
  • neural_weak (fused distance 0.15 to 0.50): Likely the same image after significant transformation.
  • perceptual_strong (dHash Hamming distance 0 to 5): Very likely the same image.
  • perceptual_weak (dHash Hamming distance 6 to 10): Moderate confidence.

/verify/image runs both paths in parallel and returns the strongest match. Neural beats classical when strong; classical wins on tie; weak neural only wins when classical found nothing. Benchmark: 97.2% recall, 0.09% false-positive rate on the full corpus (classical alone was 54.6% / 0.6%).

Platform Endpoints

All require authentication.

POST/platform/link

Start platform verification. Body: {platform, platform_username, platform_url}. Returns a verification code to post publicly.

POST/platform/verify/{link_id}

Submit proof URL showing the verification code. Body: {proof_url}.

GET/platform/links

List your platform links and their status (pending, verified, rejected).

DELETE/platform/link/{link_id}

Remove a platform link.

Supported Platforms

twitter, pixiv, deviantart, artstation, instagram

Dispute Endpoints

POST/dispute/{lantern_id}

File a dispute. Body: {disputant_email, claim_description, evidence_urls, disputant_name?, evidence_notes?}. Throttled: 5/hour per IP. No auth required.

GET/dispute/{lantern_id}

List public disputes filed against a registration. No auth required.

GET/disputes/my

List disputes filed against your content. Auth required.

Rate Limits

EndpointLimitWindow
Public verify endpoints60 requestsper minute per IP
Config endpoints (/config/*)60 requestsper minute per IP
Login5 attemptsper 15 minutes per email
Content registration10 registrationsper day per user
Reports10 filingsper hour per IP
Disputes5 filingsper hour per IP
Feedback widget (/feedback)5 submissionsper hour per IP
Data export (/auth/me/export)1 exportper hour per user

Platform API

The Platform API is for integrating Lantern into another product (a marketplace, a creator tool, a social platform). A user authorizes your client via OAuth 2.0; your backend then registers works, transfers ownership, and subscribes to webhook events on the user's behalf, with the user's wallet remaining the on-chain creator of record.

Three things you can do as a partner:

  • Register on behalf: a user posts a new work on your platform; you call POST /api/v1/register/delegated to record it on Lantern.
  • Transfer ownership: a buyer purchases a commission on your platform; you call POST /api/v1/content/{lantern_id}/transfer to move the on-chain creator from seller to buyer.
  • Subscribe to events: you register a webhook callback; we POST signed events as state changes happen on works your client originally registered.

All Platform API endpoints live under https://api.lantern-us.com/api/v1/*. All require an OAuth access token in the Authorization: Bearer ... header. Responses are JSON with snake_case fields.

Want to become a partner? Contact team@lantern-us.com. We issue client credentials manually for v1.

Code-generate your client: Download the OpenAPI 3.0 spec and feed it to openapi-generator-cli for a typed client in any language (TypeScript, Python, Go, Java, Ruby, etc.). A working TypeScript end-to-end demo lives at examples/typescript-sample/ in the Lantern repo.

Authentication

OAuth 2.0 authorization-code flow with PKCE (RFC 6749, RFC 7636) and PAR (RFC 9126). All sensitive parameters travel server-to-server via PAR, never in browser URLs. Tokens are opaque (not JWTs); revoke at any time viaPOST /oauth/revoke.

The four-step flow:

  1. PAR. Your backend POSTs to /oauth/par withclient_id, redirect_uri, scope,state, code_challenge, code_challenge_method=S256(and optional prefill_email for the auto-create branch). You receive an opaque request_uri valid for 90 seconds.
  2. Authorize. Redirect the user's browser tohttps://lantern-us.com/oauth/authorize?request_uri=<the URI>. The user sees the consent UI; on approval, we redirect to yourredirect_uri with ?code=<auth_code>&state=<state>.
  3. Token exchange. Your backend POSTs to /oauth/token withgrant_type=authorization_code, the code, the originalcode_verifier, your client_id, client_secret, and redirect_uri. You receive an access_token (1 hour TTL) and a refresh_token (90 days, rotates on use).
  4. Call APIs. Send Authorization: Bearer <access_token>on every /api/v1/* request. Refresh viagrant_type=refresh_token when the access token expires.

See the OAuth endpoints below; complete request/response shapes match RFC 6749.

POST/oauth/par

Pushed Authorization Request. Returns request_uri (90s TTL, single-use).

GET/oauth/authorize?request_uri={uri}

Browser-facing consent page. Redirects back to your redirect_uri with code+state.

POST/oauth/token

Exchange code for access+refresh tokens. Also handles grant_type=refresh_token.

POST/oauth/revoke

Invalidate an access or refresh token. Body: {token, token_type_hint?}.

GET/oauth/userinfo

Minimal user info for the access token's owner: id, email, display_name, created_at.

Rotating your client_secret

If your client_secret leaks or you want to rotate as routine hygiene, POST to/oauth/clients/me/rotate-secret with your currentcredentials (HTTP Basic or form-body, same as /oauth/token). We mint a fresh secret and return it ONCE in the response. The old secret stops working immediately for new /oauth/token calls.

Existing access + refresh tokens minted with the old secret are NOT affected by rotation; they continue working on /api/v1/* until they expire or you explicitly revoke them via /oauth/revoke. To invalidate everything in one operation, call /oauth/revoke per-token first.

POST/oauth/clients/me/rotate-secret

Rotate your client_secret. Requires current client credentials. Returns the new plaintext secret ONCE. Rate-limited 10/min per client_id.

Scopes

Each scope is requested at PAR time and shown to the user on the consent screen. Request only what you need; users can decline. To narrow scopes after the fact, ask the user to re-authorize with a smaller set.

ScopeWhat it allows
register:writeRegister new works on the user's behalf via POST /api/v1/register/delegated.
transfer:writeTransfer ownership of works the user already registered via POST /api/v1/content/{id}/transfer.
webhook:manageCreate, list, and revoke webhook subscriptions for your client.

Tokens missing the required scope receive 403 insufficient_scope.

POST /api/v1/register/delegated

Register a new work on the access token's owner's behalf. Requiresregister:write. Multipart form upload of the image bytes plus metadata. Source URL must point to where the work is published on your platform.

The user's wallet is the on-chain creator; the chain submission is signed with their server-side encrypted private key. Your client_id is recorded assource_platform = partner:<your_client_id> for the audit trail.

Request

Multipart form fields. Provide exactly one of image or image_url:

  • image: file upload (PNG, JPEG, WebP, GIF, TIFF; max 50 MB)
  • image_url: HTTPS URL of the image. We fetch server-side with 3-layer SSRF defense (scheme + IP-range check, post-connect peer IP, bounded byte read). Convenient when the work already lives at a public URL on your platform.
  • title, description, creation_tool: strings
  • source_url: the public URL where the work is posted on your platform
  • terms_agreed_version: fetched from GET /auth/terms-version
  • authorship_affirmed, age_confirmed: booleans (both must be true)
  • opt_out_training: optional boolean override

Response: 201 Created

{
  "lantern_id": "LNTN-AbCd1234",
  "content_hash": "sha256:...",
  "verify_url": "https://lantern-us.com/verify/LNTN-AbCd1234",
  "chain_tx": "0x...",
  "registered_at": "2026-05-14T12:34:56Z",
  "source_platform": "partner:your_client_id"
}

Error codes

  • 401 invalid or expired token
  • 403 insufficient_scope token lacks register:write
  • 409 content_hash already registered (returns existing lantern_id)
  • 413 file (or fetched image) over 50 MB
  • 415 unsupported content-type
  • 422 missing affirmations, stale terms_agreed_version, both or neither of image/image_url, or invalid_image_url (SSRF reject: non-public IP, non-https scheme, DNS rebinding suspected)
  • 429 rate-limit (600/min per client_id)
  • 502 chain_submission_failed on-chain register failed
  • 502 image_fetch_failed image_url returned non-2xx or the fetch timed out

POST /api/v1/content/{lantern_id}/transfer

Transfer ownership of a work registered to the access token's owner. Requirestransfer:write. The buyer is identified by email; if they don't have a Lantern account, we auto-create a pending_claim account and send them an onboarding email. The transfer is atomic with an on-chaintransferContentOwnership call.

After transfer the OLD record's status = revoked andsuperseded_by_lantern_id points at the new record. The buyer's wallet is now the on-chain creator of the new lantern_id.

Request

POST /api/v1/content/LNTN-AbCd1234/transfer
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "new_creator_email": "buyer@example.com",
  "reason_category": "commission_sale",
  "terms_agreed_version": "2026-04-23.1"
}

reason_category accepts: commission_sale (default),sold_offline, gift, other. Anything else is recorded but normalized to commission_sale in the history event.

Response: 200 OK

{
  "new_lantern_id": "LNTN-XyZw5678",
  "superseded_lantern_id": "LNTN-AbCd1234",
  "chain_tx": "0x...",
  "buyer_was_auto_created": true,
  "transferred_at": "2026-05-14T12:35:00Z"
}

Error codes

  • 403 not_owner the access token's user is not the record's creator
  • 404 not_found lantern_id doesn't exist
  • 409 record_inactive record is revoked or superseded
  • 409 record_disputed record has an open or upheld dispute
  • 422 chain_pending on-chain register hasn't confirmed yet (retry shortly)
  • 422 self-transfer (buyer email == seller email), or stale terms
  • 429 rate-limit (30/min per client_id + IP)
  • 502 chain_submission_failed on-chain transfer failed

Verify lookups

Public verify endpoints, no auth required. Use these to render a Lantern badge on your platform. ETag + 300s cache headers; mirror them in your CDN.

GET/api/v1/verify/{lantern_id}

Full provenance for a Lantern ID: display block, chain block, verify_url. 404 on unknown.

GET/api/v1/verify/hash/{content_hash}

Same as above but lookup is by SHA-256 content hash. Useful before upload (verify a local file).

GET/api/v1/verify/{lantern_id}/history

Provenance timeline (register, revoke, dispute, transfer events) for the lantern_id.

Webhooks

Subscribe a callback URL and we POST signed events when state changes occur on works your client originally registered. Requires webhook:manage. Events are scoped per-partner: you only receive events for works your client_id registered or brokered, with no cross-partner data leakage.

POST/api/v1/webhooks

Create a subscription. Body: {name, target_url (https://), event_types: [...]}. Returns the secret ONCE; store it, we can't show it again.

GET/api/v1/webhooks

List your subscriptions. Secret is never returned.

DELETE/api/v1/webhooks/{id}

Soft-revoke a subscription. Deliveries stop within seconds.

POST/api/v1/webhooks/{id}/test

Fire a synthetic test delivery (event_type='test') to confirm your receiver wiring.

Event types

  • content_registered: a new work was registered by your client
  • content_revoked: a work registered by your client was revoked (by creator, admin, or account deletion)
  • content_disputed: a dispute was filed or resolved on a work registered by your client
  • content_transferred: ownership of a work registered by your client transferred to a new owner
  • content_embedded: Lantern's neural matcher has fingerprinted the work; it's now searchable for unauthorized copies. Fires at most once per record (subsequent re-embeds don't refire).
  • content_source_confirmed: our background worker verified the work is publicly hosted at the source_url the user claimed. Useful for partners that want to wait for this gate before showing a "Verified Source" badge.

Inspecting your delivery history

Partners can read back recent delivery attempts viaGET /api/v1/webhooks/{subscription_id}/deliveries, useful for debugging your receiver. Returns up to 100 rows per call withstatus, attempts, last_status, and a 500-char snippet of the response body, newest-first. Query params:status (filter by pending/in_progress/success/failed/dead_letter),limit (default 25, max 100), offset. Use thenext_offset field in the response to page.

We don't return the full payload in this endpoint; partners re-derive from their own receiver logs if needed.

GET/api/v1/webhooks/{subscription_id}/deliveries

List recent delivery attempts. Query: status, limit (max 100), offset.

Delivery shape

HTTP POST to your target_url with these headers:

X-Lantern-Signature: sha256=<hex>
X-Lantern-Event: content_registered
X-Lantern-Delivery: <delivery_uuid>
Content-Type: application/json
User-Agent: Lantern-Webhooks/1.0

Body:

{
  "event": "content_registered",
  "occurred_at": "2026-05-14T12:34:56Z",
  "lantern_id": "LNTN-AbCd1234",
  "data": {
    "verify_url": "https://lantern-us.com/verify/LNTN-AbCd1234",
    "actor": { "wallet_address": "0x...", "display_name": "alice" },
    "chain_tx": "0x...",
    "reason_category": "original_work",
    "metadata": { ... }
  }
}

Verifying the signature

Compute HMAC-SHA256(secret, raw_body) and compare in constant time against the hex portion of X-Lantern-Signature. Reject the request if they don't match.

Node.js example

import crypto from "node:crypto";

function verify(rawBody, headerSig, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  // Constant-time compare to defeat timing attacks
  const a = Buffer.from(expected);
  const b = Buffer.from(headerSig);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python example

import hmac, hashlib

def verify(raw_body: bytes, header_sig: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header_sig)

Retry policy

We expect a 2xx response within 10 seconds. Any non-2xx (or timeout) is retried with exponential backoff: 1m, 2m, 4m, 8m, 16m, 32m, 64m, 128m, up to 8 attempts, ~4 hours and 15 minutes total. After that, the delivery is marked dead_letter and won't retry. Build idempotency by deduplicating on the X-Lantern-Delivery UUID.

Errors + rate limits

Error envelope

4xx responses use a consistent shape:

{
  "detail": {
    "error": "insufficient_scope",
    "error_description": "Token missing required scope: transfer:write"
  }
}

Match on error (a stable machine string); showerror_description as a hint in your UI if needed.

Rate limits

EndpointLimitWindow
POST /api/v1/register/delegated600 requestsper minute per client_id
POST /api/v1/content/{id}/transfer30 requestsper minute per (client_id, IP)
Webhook CRUD (/api/v1/webhooks/*)100 requestsper minute per (client_id, IP)
GET /api/v1/verify/*60 requestsper minute per IP

429 responses include Retry-After (seconds). Back off and retry; we don't blacklist clients for transient limit hits.