Authentication
Most write operations against the Highlight API require authentication. Public read endpoints (getting a live collection, listing its tokens, querying platform config) work without any credentials.
The backend accepts two credential types:
- JWT access tokens — obtained by signing in with a wallet (SIWE) or a Privy token. Short-lived.
- API keys — long-lived, user-created credentials for server-to-server use.
Both are passed as Authorization: Bearer <token> headers. The SDK wires this for you.
Sign in with SIWE (wallet)
Section titled “Sign in with SIWE (wallet)”SIWE (Sign In With Ethereum, EIP-4361) is the canonical way to authenticate a wallet. It’s a two-step flow:
import { createHighlightClient } from "@highlightxyz/sdk";import { createSiweMessage } from "viem/siwe";
const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz",});
// 1. Get a short-lived nonceconst nonceRes = await client.user.siwe.nonce();const { nonce } = nonceRes.data!;
// 2. Build and sign a SIWE message with the user's walletconst message = createSiweMessage({ address: account.address, chainId: 1, domain: "your-app.example.com", nonce, uri: "https://your-app.example.com", version: "1", issuedAt: new Date(), expirationTime: new Date(Date.now() + 10 * 60 * 1000),});const signature = await account.signMessage({ message });
// 3. Exchange the signed message for a JWTconst signinRes = await client.user.signin({ body: { providerType: "Siwe", message, signature },});const { tokens, user } = signinRes.data!;// tokens.accessToken, tokens.refreshTokenThe domain in your SIWE message must match the host the backend expects. During local development this is typically the API’s public host.
Sign in with Privy
Section titled “Sign in with Privy”If your app uses Privy for wallet auth, trade the Privy id_token for a Highlight JWT:
const signinRes = await client.user.signin({ body: { providerType: "Privy", token: privyIdToken },});const { tokens, user } = signinRes.data!;The backend verifies the Privy token out-of-band and issues its own JWT.
Using the token
Section titled “Using the token”Configure the SDK once, then every subsequent call is authenticated:
const authedClient = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", auth: () => accessToken, security: [{ type: "http", scheme: "bearer" }],});
// Every call now carries Authorization: Bearer <accessToken>await authedClient.collection.list({ blockchains: [8453], testnet: false, types: ["OpenEdition"],});The auth callback is invoked on every request, so you can refresh tokens in-flight by reading from a cache or async source.
Access token lifetime
Section titled “Access token lifetime”JWT access tokens expire after 15 minutes. Refresh by calling signin again (or by holding a refresh token and implementing your own rotation). For long-running processes, re-authenticate on a timer well before the 15-minute boundary — the e2e tests re-sign every 10 minutes.
API keys
Section titled “API keys”For server-to-server automation where repeated SIWE is inconvenient, mint an API key once and reuse it.
// Must be authenticated with a JWT to create keysconst keyRes = await authedClient.apiKey.create({ name: "prod-automation", expiresAt: "2027-01-01T00:00:00Z", // optional});const { plaintext, apiKey } = keyRes.data!;// plaintext is shown ONCE — save it nowList and revoke:
const keys = await authedClient.apiKey.list();await authedClient.apiKey.revoke({ apiKeyId: "key-uuid" });Use an API key like a JWT — pass the returned plaintext as the bearer token.
Public endpoints
Section titled “Public endpoints”The following endpoints work without any Authorization header:
GET /healthGET /config/chainsGET /config/system-contractGET /mechanicandGET /mechanic/:idPOST /user/signinandPOST /user/signin/siwe/nonceGET /collection/:highlightId— when the collection isLive(drafts require ownership)GET /collection/:highlightId/tokens— when the parent collection is publicGET /media/:mediaId— via the image-transformation path, not the metadata endpoint
Everything else requires auth.
Authorization errors
Section titled “Authorization errors”401 Unauthorized— missing/invalid/expired token.403 Forbidden— authenticated but not the owner of the resource (e.g. someone else’s draft collection).
See the error reference for the full list of typed errors.
Related
Section titled “Related”- Developer quick start — First authenticated call.
- SDK setup — Client configuration reference.
- REST API: user-signin — Full OpenAPI schema.