Public Key + JWT Authentication#

Public key JWT authentication lets client-side (browser) apps call the Enconvert API securely: you exchange your public key (pk_) for a short-lived JWT access token via POST /v1/auth/token, then send that token in the Authorization: Bearer <token> header on API requests. Because a public key is visible to end users, it cannot call the API directly — a valid JWT is always required, and tokens are only issued to whitelisted domains. Access tokens expire after 1 hour and are refreshed via POST /v1/auth/refresh using an HttpOnly refresh-token cookie.

How It Works#

  1. Exchange your public key (pk_) for a JWT access token by calling POST /v1/auth/token.
  2. Use the JWT token in the Authorization: Bearer <token> header on API requests.
  3. Refresh the token automatically before it expires using POST /v1/auth/refresh.
  4. Domain whitelisting ensures that only requests originating from your approved domains are accepted.

Step 1: Exchange Public Key for JWT#

Send your public key and the parent origin to obtain an access token.

Endpoint#

POST /v1/auth/token

Request Headers#

Header Value Description
X-API-Key pk_your_public_key Your public API key

JavaScript Example#

async function getToken() {
  const response = await fetch("https://api.enconvert.com/v1/auth/token", {
    method: "POST",
    headers: {
      "X-API-Key": "pk_your_public_key",
    },
    credentials: "include",
  });

  if (!response.ok) {
    throw new Error(`Token exchange failed: ${response.status}`);
  }

  const data = await response.json();
  return data.token;
}
Important: You must include credentials: "include" in the fetch options. This ensures the refresh token cookie is stored by the browser, which is required for automatic token refresh.

Response#

{
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

The response also sets an HttpOnly cookie containing the refresh token. This cookie is managed automatically by the browser and is used when refreshing the access token.

Step 2: Use the JWT Token#

Include the JWT token in the Authorization header as a Bearer token on all subsequent API requests.

async function convertUrlToPdf(token, url) {
  const response = await fetch("https://api.enconvert.com/v1/convert/url-to-pdf", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url }),
  });

  return await response.json();
}

// Usage
const token = await getToken();
const result = await convertUrlToPdf(token, "https://example.com");
console.log(result.presigned_url);

Step 3: Automatic Token Refresh#

Access tokens expire after a short period. Use the refresh endpoint to obtain a new access token without requiring the user to re-authenticate.

Endpoint#

POST /v1/auth/refresh

The refresh token is sent automatically via the HttpOnly cookie that was set during the initial token exchange. No request body or additional headers are needed.

JavaScript Auto-Refresh Example#

class EnconvertClient {
  constructor(publicKey) {
    this.publicKey = publicKey;
    this.token = null;
    this.tokenExpiry = null;
  }

  async getToken() {
    const response = await fetch("https://api.enconvert.com/v1/auth/token", {
      method: "POST",
      headers: {
        "X-API-Key": this.publicKey,
        "X-Parent-Origin": window.location.origin,
      },
      credentials: "include",
    });

    const data = await response.json();
    this.token = data.token;
    // Set expiry to 55 minutes (refresh before the 1-hour expiry)
    this.tokenExpiry = Date.now() + 55 * 60 * 1000;
    return this.token;
  }

  async refreshToken() {
    const response = await fetch("https://api.enconvert.com/v1/auth/refresh", {
      method: "POST",
      credentials: "include",
    });

    if (!response.ok) {
      // Refresh token expired, re-authenticate
      return await this.getToken();
    }

    const data = await response.json();
    this.token = data.token;
    this.tokenExpiry = Date.now() + 55 * 60 * 1000;
    return this.token;
  }

  async getValidToken() {
    if (!this.token || Date.now() >= this.tokenExpiry) {
      if (this.token) {
        return await this.refreshToken();
      }
      return await this.getToken();
    }
    return this.token;
  }

  async convert(endpoint, body) {
    const token = await this.getValidToken();
    const response = await fetch(`https://api.enconvert.com${endpoint}`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    return await response.json();
  }
}

// Usage
const client = new EnconvertClient("pk_your_public_key");
const result = await client.convert("/v1/convert/url-to-pdf", {
  url: "https://example.com",
});

Token Lifetimes#

Token Lifetime Storage
Access token 1 hour Returned in the JSON response body; store in memory
Refresh token 7 days Set as an HttpOnly cookie; managed by the browser

Domain Whitelisting#

Public keys are restricted to specific domains configured in your dashboard.

Matching Rules#

  • Exact match: https://example.com matches only https://example.com.
  • Wildcard subdomains: https://*.example.com matches https://app.example.com, https://staging.example.com, etc.
  • Port-specific: http://localhost:3000 matches only that exact origin including the port.

Examples#

Whitelisted Domain Matches Does Not Match
https://example.com https://example.com https://www.example.com
https://*.example.com https://app.example.com, https://dev.example.com https://example.com
http://localhost:3000 http://localhost:3000 http://localhost:8080

Security Features#

  • Short-lived tokens: Access tokens expire after 1 hour, limiting the window of exposure if a token is compromised.
  • HttpOnly refresh cookies: Refresh tokens are stored in HttpOnly cookies, making them inaccessible to JavaScript and resistant to XSS attacks.
  • Domain restrictions: Tokens are only issued when the request originates from a whitelisted domain.
  • No direct API access: Public keys alone cannot call conversion endpoints. A valid JWT is always required.

Public Key Restrictions#

Public key authentication has the following limitations compared to private keys:

  • Synchronous only: Only synchronous conversion endpoints are available. Async operations, job polling, and webhooks are not supported.
  • Single URL per request: Batch processing is not available. Each request may convert only one URL or file.
  • Direct download: Responses provide a presigned_url for immediate download. There is no option for custom storage destinations.
Best Practices:
  • Always store access tokens in memory only. Never persist them to localStorage or sessionStorage.
  • Implement automatic token refresh to avoid interruptions during user sessions.
  • Keep your whitelisted domains list as specific as possible. Avoid broad wildcards.
  • Use credentials: "include" on all fetch requests to ensure cookies are sent and received correctly.
  • Handle token refresh failures gracefully by falling back to a full re-authentication with the public key.

Frequently asked questions#

How do I exchange an API key for a JWT token?#

Call POST /v1/auth/token with your public key in the X-API-Key header and credentials: "include" in the fetch options. The response returns a token in the JSON body and sets an HttpOnly cookie containing the refresh token.

How long do access tokens and refresh tokens last?#

Access tokens expire after 1 hour and should be stored in memory only. Refresh tokens last 7 days and are set as an HttpOnly cookie managed by the browser.

Why does my token refresh fail without credentials: "include"?#

The refresh token is stored in an HttpOnly cookie set during the initial token exchange, and POST /v1/auth/refresh relies on the browser sending that cookie automatically. If you omit credentials: "include" from your fetch requests, the cookie is not stored or sent — when a refresh fails, fall back to a full re-authentication with your public key.

Can I use wildcard subdomains in the domain whitelist?#

Yes. https://*.example.com matches https://app.example.com and https://staging.example.com, but not https://example.com itself. Exact matches and port-specific origins like http://localhost:3000 are also supported.

Can I use a public key for async or batch conversions?#

No. Public key authentication supports synchronous conversion endpoints only, with a single URL or file per request; async operations, job polling, webhooks, and batch processing require a private key. Responses provide a presigned_url for immediate download.