> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs-v3.openfx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Generate ES256-signed JWT bearer tokens to access the OpenFX v2 API.

To access the v2 API, authentication via bearer tokens is required. Tokens are generated by creating API keys through the dashboard. Signing the key with `ES256` produces a short-lived JWT that you attach to every request.

<Tip>
  Migrating from v1? The token format is identical, but v2 also requires an `Idempotency-Key` header on state-changing endpoints. See the [migration guide](/v2/migration-v1-to-v2) for details.
</Tip>

## Generating a JWT bearer token

Once an API key is created, a JWT bearer token can be generated by signing the key. This token is then used to authenticate requests to the v2 API.

To generate a JWT bearer token, sign an API key using the `ES256` algorithm with the `name` and `privateKey` from the downloaded API Key JSON file. The following snippets show how to create a JWT bearer token in various languages.

<CodeGroup>
  ```javascript createAuthToken.js theme={null}
  const { sign } = require("jsonwebtoken");
  const crypto = require("crypto");

  export const createAuthToken = () => {
    const keyName = "org/{org-id}/apiKey/{api-key-id}"; // Replace with `name`
    const keySecret = "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----"; // Replace with the `privateKey`.
    const serviceName = "developer-api";

    const now = Math.floor(Date.now() / 1000);

    const payload = {
      aud: [serviceName],
      iss: "openfx",
      sub: keyName,
      iat: now,
      nbf: now,
      exp: now + 120, // Max 2 minutes allowed
    };

    const options = {
      header: {
        alg: "ES256",
        typ: "JWT",
        kid: keyName,
        nonce: crypto.randomBytes(16).toString("hex"),
      },
    };

    const token = sign(payload, keySecret, options);

    return token;
  };
  ```

  ```typescript createAuthToken.ts theme={null}
  import { sign, type JwtHeader } from "jsonwebtoken";
  import crypto from "node:crypto";

  export const createAuthToken = (): string => {
    const keyName = "org/{org-id}/apiKey/{api-key-id}"; // Replace with `name`
    const keySecret = "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----"; // Replace with the `privateKey`.
    const serviceName = "developer-api";

    const now = Math.floor(Date.now() / 1000);

    const payload = {
      aud: [serviceName],
      iss: "openfx",
      sub: keyName,
      iat: now,
      nbf: now,
      exp: now + 120, // Max 2 minutes allowed
    };

    const options = {
      header: {
        alg: "ES256",
        typ: "JWT",
        kid: keyName,
        nonce: crypto.randomBytes(16).toString("hex"),
      } as JwtHeader,
    };

    const token = sign(payload, keySecret, options);

    return token;
  };
  ```

  ```python create_auth_token.py theme={null}
  import time
  import secrets
  import jwt


  def create_auth_token() -> str:
      key_name = "org/{org-id}/apiKey/{api-key-id}"  # Replace with `name`
      key_secret = "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----"  # Replace with the `privateKey`.

      service_name = "developer-api"
      now = int(time.time())

      payload = {
          "aud": [service_name],
          "iss": "openfx",
          "sub": key_name,
          "iat": now,
          "nbf": now,
          "exp": now + 120,  # 2-minute TTL
      }

      headers = {
          "kid": key_name,
          "typ": "JWT",
          "nonce": secrets.token_hex(16),
      }

      token = jwt.encode(payload, key_secret, algorithm="ES256", headers=headers)
      return token
  ```

  ```java AuthTokenUtil.java theme={null}
  import io.jsonwebtoken.Jwts;
  import io.jsonwebtoken.SignatureAlgorithm;
  import io.jsonwebtoken.security.Keys;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.security.KeyFactory;
  import java.security.PrivateKey;
  import java.security.SecureRandom;
  import java.security.spec.PKCS8EncodedKeySpec;
  import java.time.Instant;
  import java.util.Arrays;
  import java.util.Base64;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.Map;

  public class AuthTokenUtil {

      private static final String KEY_NAME = "org/{org-id}/apiKey/{api-key-id}"; // Replace with `name`
      private static final String KEY_SECRET = "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----"; // Replace with the actual `privateKey`
      private static final String SERVICE_NAME = "developer-api";

      public static String createAuthToken() {
          try {
              long now = Instant.now().getEpochSecond();

              // Generate random nonce
              SecureRandom random = new SecureRandom();
              byte[] nonceBytes = new byte[16];
              random.nextBytes(nonceBytes);
              String nonce = bytesToHex(nonceBytes);

              // Parse the private key
              PrivateKey privateKey = parsePrivateKey(KEY_SECRET);

              // Create JWT with custom header (exactly matching TypeScript structure)
              Map<String, Object> header = new HashMap<>();
              header.put("kid", KEY_NAME);
              header.put("nonce", nonce);
              header.put("typ", "JWT");
              // Note: alg is set via signWith() method, not in header map

              String token = Jwts.builder()
                      .setHeader(header)
                      .claim("aud", Arrays.asList(SERVICE_NAME))
                      .setIssuer("openfx")
                      .setSubject(KEY_NAME)
                      .setNotBefore(new Date(now * 1000))
                      .setExpiration(new Date((now + 120) * 1000)) // Max 2 minutes allowed
                      .setIssuedAt(new Date(now * 1000))
                      .signWith(privateKey, SignatureAlgorithm.ES256)
                      .compact();

              return token;

          } catch (Exception e) {
              throw new RuntimeException("Failed to create auth token", e);
          }
      }

      private static PrivateKey parsePrivateKey(String privateKeyPem) throws Exception {
          String privateKeyContent = privateKeyPem
                  .replace("-----BEGIN PRIVATE KEY-----", "")
                  .replace("-----END PRIVATE KEY-----", "")
                  .replaceAll("\\s", "");

          byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
          PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
          KeyFactory keyFactory = KeyFactory.getInstance("EC");
          return keyFactory.generatePrivate(keySpec);
      }

      private static String bytesToHex(byte[] bytes) {
          StringBuilder result = new StringBuilder();
          for (byte b : bytes) {
              result.append(String.format("%02x", b));
          }
          return result.toString();
      }
  }
  ```

  ```go createAuthToken.go theme={null}
  package utils

  import (
  	"crypto/ecdsa"
  	"crypto/rand"
  	"crypto/x509"
  	"encoding/hex"
  	"encoding/pem"
  	"fmt"
  	"time"

  	"github.com/golang-jwt/jwt/v5"
  )

  // CreateAuthToken creates a JWT token with ES256 algorithm
  func CreateAuthToken() (string, error) {
  	keyName := "org/{org-id}/apiKey/{api-key-id}" // Replace with `name`
  	keySecret := "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----" // Replace with the `privateKey`
  	serviceName := "developer-api"

  	now := time.Now().Unix()

  	// Parse the private key
  	privateKey, err := parsePrivateKey(keySecret)
  	if err != nil {
  		return "", fmt.Errorf("failed to parse private key: %w", err)
  	}

  	// Generate random nonce
  	nonceBytes := make([]byte, 16)
  	if _, err := rand.Read(nonceBytes); err != nil {
  		return "", fmt.Errorf("failed to generate nonce: %w", err)
  	}
  	nonce := hex.EncodeToString(nonceBytes)

  	// Create claims
  	claims := jwt.MapClaims{
  		"aud": []string{serviceName},
  		"iss": "openfx",
  		"sub": keyName,
  		"iat": now,
  		"nbf": now,
  		"exp": now + 120, // 2 minutes
  	}

  	// Create token with custom header
  	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
  	token.Header["kid"] = keyName
  	token.Header["nonce"] = nonce
  	token.Header["typ"] = "JWT"

  	// Sign the token
  	tokenString, err := token.SignedString(privateKey)
  	if err != nil {
  		return "", fmt.Errorf("failed to sign token: %w", err)
  	}

  	return tokenString, nil
  }

  // parsePrivateKey parses a PEM-encoded ECDSA private key
  func parsePrivateKey(keyPEM string) (*ecdsa.PrivateKey, error) {
  	block, _ := pem.Decode([]byte(keyPEM))
  	if block == nil {
  		return nil, fmt.Errorf("failed to parse PEM block containing the key")
  	}

  	privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  	if err != nil {
  		// Try parsing as EC private key if PKCS8 fails
  		privateKey, err = x509.ParseECPrivateKey(block.Bytes)
  		if err != nil {
  			return nil, fmt.Errorf("failed to parse private key: %w", err)
  		}
  	}

  	ecdsaKey, ok := privateKey.(*ecdsa.PrivateKey)
  	if !ok {
  		return nil, fmt.Errorf("not an ECDSA private key")
  	}

  	return ecdsaKey, nil
  }
  ```
</CodeGroup>

Run the code to generate the JWT bearer token.

<Note>
  Retrieve the organization ID from the `orgId` parameter and the private key from the `privateKey` parameter in the JSON file downloaded during API key creation.
</Note>

## Using the JWT bearer token

Once the JWT bearer token is generated, include it in the `Authorization` header of every request:

```bash theme={null}
curl -L "https://api.openfx.com/v2/<required-route>" -H "Authorization: Bearer {JWT_Bearer_Token}"
```

Replace `{JWT_Bearer_Token}` with the generated token.

<Warning>
  * JWT bearer tokens expire **2 minutes** after generation. Generate a new token for continued API access.
  * Setting a longer expiry via `exp` is not allowed.
  * A unique JWT must be generated for each API request; reuse is not permitted.
</Warning>

## Idempotency

v2 also requires an `Idempotency-Key` header on every state-changing request (POST, PUT, PATCH, DELETE). See the [migration guide](/v2/migration-v1-to-v2) for the full list of affected endpoints, the cache TTL for each, and the error codes you should handle.
