---
seo_title: Public Key JWT Authentication — Client-Side API Auth | EnConvert
meta_desc: Exchange a pk_ public key for a JWT bearer token via POST /v1/auth/token for client-side API auth in browsers. Domain whitelisting, 1-hour tokens, auto refresh.
keywords: jwt bearer token api authentication, public key client-side api auth, exchange api key for jwt token, /v1/auth/token endpoint, httponly refresh token cookie, jwt token refresh javascript, domain whitelisting api key, short-lived access token browser
---

# 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

```javascript
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;
}
```

<div class="alert alert-warning">
<strong>Important:</strong> You must include <code>credentials: "include"</code> in the fetch options. This ensures the refresh token cookie is stored by the browser, which is required for automatic token refresh.
</div>

### Response

```json
{
  "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.

```javascript
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

```javascript
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.

<div class="alert alert-warning">
<strong>Best Practices:</strong>
<ul>
  <li>Always store access tokens in memory only. Never persist them to <code>localStorage</code> or <code>sessionStorage</code>.</li>
  <li>Implement automatic token refresh to avoid interruptions during user sessions.</li>
  <li>Keep your whitelisted domains list as specific as possible. Avoid broad wildcards.</li>
  <li>Use <code>credentials: "include"</code> on all fetch requests to ensure cookies are sent and received correctly.</li>
  <li>Handle token refresh failures gracefully by falling back to a full re-authentication with the public key.</li>
</ul>
</div>

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