Skip to content

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:

  1. JWT access tokens — obtained by signing in with a wallet (SIWE) or a Privy token. Short-lived.
  2. 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.

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 nonce
const nonceRes = await client.user.siwe.nonce();
const { nonce } = nonceRes.data!;
// 2. Build and sign a SIWE message with the user's wallet
const 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 JWT
const signinRes = await client.user.signin({
body: { providerType: "Siwe", message, signature },
});
const { tokens, user } = signinRes.data!;
// tokens.accessToken, tokens.refreshToken

The domain in your SIWE message must match the host the backend expects. During local development this is typically the API’s public host.

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.

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.

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.

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 keys
const 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 now

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

The following endpoints work without any Authorization header:

  • GET /health
  • GET /config/chains
  • GET /config/system-contract
  • GET /mechanic and GET /mechanic/:id
  • POST /user/signin and POST /user/signin/siwe/nonce
  • GET /collection/:highlightId — when the collection is Live (drafts require ownership)
  • GET /collection/:highlightId/tokens — when the parent collection is public
  • GET /media/:mediaId — via the image-transformation path, not the metadata endpoint

Everything else requires auth.

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