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.
- Create an account on the Dashboard (requires an invite key)
- Link your platform accounts to prove you own them
- Upload and register your art
- Share your Lantern ID or let the extension detect it automatically
- Manage authorized apps anytime at Connected Apps
Register Art
Registration creates a blockchain record tied to your identity.
- Go to the Dashboard and sign in
- Upload your image file (PNG, JPEG, WebP, GIF, or TIFF, up to 50MB)
- Provide the URL where the work is posted (must match a verified platform account)
- Lantern computes a SHA-256 hash, perceptual hashes (dHash + pHash), and three neural fingerprints (SSCD, CLIP, DINOv3) used for similarity matching
- The SHA-256 hash is registered on the Base blockchain with an EIP-712 signature
- 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.
- Go to Dashboard and click "Link a Platform"
- Select the platform and enter your username and profile URL
- You receive a verification code
- Post the code in a public post on that platform
- Submit the proof URL (link to the public post containing the code)
- 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.
/verify/{lantern_id}Look up a registration by Lantern ID. Returns creator info, metadata, chain status, dispute status, and perceptual hashes if available.
/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.
/verify/imageUpload-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.
/creator/{wallet_address}List content registered by a wallet address. Supports offset and limit query params for pagination.
/config/platformsReturns the hostname-to-platform map and display labels used by the extension to auto-detect supported sites. Cached 1h client-side.
/config/featuresReturns 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.
/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.
/report/{lantern_id}Submit a report. Body: {reason, evidence_url?, reporter_email?}.
/healthReturns {status: 'ok'} if the API is running.
Auth Endpoints
Sign in + session
/auth/registerCreate account. Body: {email, password, display_name, invite_key}. Returns JWT token and user info.
/auth/loginSign in. Body: {email, password}. Returns JWT token. Throttled: 5 attempts per 15 minutes per email.
/auth/meGet current user info including content count, linked platforms, wallet address, and admin status. Requires auth.
/auth/revoke-my-tokensInvalidate 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
/auth/password-rulesReturns the password policy (minimum length, character class rules) used by the frontend register form. Public; no auth required.
/auth/terms-versionReturns 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
/auth/mePermanently delete your account. Body: {content_disposition: 'preserve' | 'anonymize' | 'revoke_all'}. Requires recent reauth. Honors the disposition choice on your registered content.
/auth/me/exportDSAR 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
/auth/google/callbackComplete a Google OAuth sign-in. Body: {credential: <Google ID token JWT>}. Returns a Lantern JWT for the matched (or auto-created) account.
/auth/google/linkLink a Google account to an existing email-password account. Requires auth. Body: {credential: <Google ID token JWT>}.
Connected apps
/auth/connected-appsList 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.
/auth/connected-apps/{client_id}Revoke a partner app's access. Invalidates outstanding access + refresh tokens for that client. Returns 204.
/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
/registerRegister new content. Multipart form: image file + source_platform + source_url + optional title, description, creation_tool. Requires a verified platform link. Limited to 10/day.
/revoke/{lantern_id}Revoke your own registration. This is permanent and triggers an on-chain revocation.
/reactivate/{lantern_id}Reactivate a self-revoked work. Available via the V2 contract. Only the original creator can reactivate.
/{lantern_id}/transferTransfer 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
/{lantern_id}/backfill-hashAdd perceptual hashes to an existing registration. Upload the original file. Only the creator can backfill. Verifies SHA-256 matches.
/{lantern_id}/embedBackfill 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.
/{lantern_id}/c2pa-signSign the content with Lantern's C2PA certificate. Returns a .c2pa manifest URL embeddable in the original file. Creator-only.
Recovery + introspection
/register/recover-orphanRecover 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.
/{lantern_id}/training-set-checkTier 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
/statsAdmin-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.
/verify/similar/{dhash_hex}?threshold=10Search 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.
/verify/similar/batchBatch 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.
/platform/linkStart platform verification. Body: {platform, platform_username, platform_url}. Returns a verification code to post publicly.
/platform/verify/{link_id}Submit proof URL showing the verification code. Body: {proof_url}.
/platform/linksList your platform links and their status (pending, verified, rejected).
/platform/link/{link_id}Remove a platform link.
Supported Platforms
twitter, pixiv, deviantart, artstation, instagram
Dispute Endpoints
/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.
/dispute/{lantern_id}List public disputes filed against a registration. No auth required.
/disputes/myList disputes filed against your content. Auth required.
Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
| Public verify endpoints | 60 requests | per minute per IP |
| Config endpoints (/config/*) | 60 requests | per minute per IP |
| Login | 5 attempts | per 15 minutes per email |
| Content registration | 10 registrations | per day per user |
| Reports | 10 filings | per hour per IP |
| Disputes | 5 filings | per hour per IP |
| Feedback widget (/feedback) | 5 submissions | per hour per IP |
| Data export (/auth/me/export) | 1 export | per 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/delegatedto record it on Lantern. - Transfer ownership: a buyer purchases a commission on your platform; you call
POST /api/v1/content/{lantern_id}/transferto 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:
- PAR. Your backend POSTs to
/oauth/parwithclient_id,redirect_uri,scope,state,code_challenge,code_challenge_method=S256(and optionalprefill_emailfor the auto-create branch). You receive an opaquerequest_urivalid for 90 seconds. - Authorize. Redirect the user's browser to
https://lantern-us.com/oauth/authorize?request_uri=<the URI>. The user sees the consent UI; on approval, we redirect to yourredirect_uriwith?code=<auth_code>&state=<state>. - Token exchange. Your backend POSTs to
/oauth/tokenwithgrant_type=authorization_code, thecode, the originalcode_verifier, yourclient_id,client_secret, andredirect_uri. You receive anaccess_token(1 hour TTL) and arefresh_token(90 days, rotates on use). - Call APIs. Send
Authorization: Bearer <access_token>on every/api/v1/*request. Refresh viagrant_type=refresh_tokenwhen the access token expires.
See the OAuth endpoints below; complete request/response shapes match RFC 6749.
/oauth/parPushed Authorization Request. Returns request_uri (90s TTL, single-use).
/oauth/authorize?request_uri={uri}Browser-facing consent page. Redirects back to your redirect_uri with code+state.
/oauth/tokenExchange code for access+refresh tokens. Also handles grant_type=refresh_token.
/oauth/revokeInvalidate an access or refresh token. Body: {token, token_type_hint?}.
/oauth/userinfoMinimal 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.
/oauth/clients/me/rotate-secretRotate 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.
| Scope | What it allows |
|---|---|
register:write | Register new works on the user's behalf via POST /api/v1/register/delegated. |
transfer:write | Transfer ownership of works the user already registered via POST /api/v1/content/{id}/transfer. |
webhook:manage | Create, 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: stringssource_url: the public URL where the work is posted on your platformterms_agreed_version: fetched fromGET /auth/terms-versionauthorship_affirmed,age_confirmed: booleans (both must betrue)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
401invalid or expired token403 insufficient_scopetoken lacksregister:write409content_hash already registered (returns existing lantern_id)413file (or fetched image) over 50 MB415unsupported content-type422missing affirmations, staleterms_agreed_version, both or neither ofimage/image_url, orinvalid_image_url(SSRF reject: non-public IP, non-https scheme, DNS rebinding suspected)429rate-limit (600/min per client_id)502 chain_submission_failedon-chain register failed502 image_fetch_failedimage_urlreturned 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_ownerthe access token's user is not the record's creator404 not_foundlantern_id doesn't exist409 record_inactiverecord is revoked or superseded409 record_disputedrecord has an open or upheld dispute422 chain_pendingon-chain register hasn't confirmed yet (retry shortly)422self-transfer (buyer email == seller email), or stale terms429rate-limit (30/min per client_id + IP)502 chain_submission_failedon-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.
/api/v1/verify/{lantern_id}Full provenance for a Lantern ID: display block, chain block, verify_url. 404 on unknown.
/api/v1/verify/hash/{content_hash}Same as above but lookup is by SHA-256 content hash. Useful before upload (verify a local file).
/api/v1/verify/{lantern_id}/historyProvenance 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.
/api/v1/webhooksCreate a subscription. Body: {name, target_url (https://), event_types: [...]}. Returns the secret ONCE; store it, we can't show it again.
/api/v1/webhooksList your subscriptions. Secret is never returned.
/api/v1/webhooks/{id}Soft-revoke a subscription. Deliveries stop within seconds.
/api/v1/webhooks/{id}/testFire a synthetic test delivery (event_type='test') to confirm your receiver wiring.
Event types
content_registered: a new work was registered by your clientcontent_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 clientcontent_transferred: ownership of a work registered by your client transferred to a new ownercontent_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 thesource_urlthe 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.
/api/v1/webhooks/{subscription_id}/deliveriesList 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
| Endpoint | Limit | Window |
|---|---|---|
POST /api/v1/register/delegated | 600 requests | per minute per client_id |
POST /api/v1/content/{id}/transfer | 30 requests | per minute per (client_id, IP) |
Webhook CRUD (/api/v1/webhooks/*) | 100 requests | per minute per (client_id, IP) |
GET /api/v1/verify/* | 60 requests | per minute per IP |
429 responses include Retry-After (seconds). Back off and retry; we don't blacklist clients for transient limit hits.