Skip to main content
The Kynva API authenticates every request with a bearer token in the Authorization header:
Authorization: Bearer <token>
There are two kinds of token. Pick based on who is making the call:
Use caseToken typeLifetime
Server-to-server (your backend, CI, scripts)API key (kyn_live_... / kyn_test_...)Long-lived — until rotated
End-user request from your frontendJWT (issued by your auth provider)Short — minutes
Never put an API key in browser, mobile, or desktop client code. API keys grant full access to your workspace. If you need to call Kynva from a user’s device, mint a JWT on your backend and forward that.

Method 1 — API key (Bearer)

API keys live in Settings → API Keys. Each key has:
FieldNotes
Prefixkyn_live_ (charges credits) or kyn_test_ (free, sandbox). Legacy rf_live_ / rf_test_ keys remain valid.
ScopesWhat the key can do (render:write, brands:read, etc.). Scope down whenever possible.
Created / Last usedVisible in the dashboard. Use this to find stale keys.

Send the key

curl https://api.kynva.ai/api/v1/facade/health \
  -H "Authorization: Bearer kyn_live_abc123..."
const res = await fetch("https://api.kynva.ai/api/v1/facade/health", {
  headers: {
    Authorization: `Bearer ${process.env.KYNVA_API_KEY}`,
  },
});
import os, requests

res = requests.get(
    "https://api.kynva.ai/api/v1/facade/health",
    headers={"Authorization": f"Bearer {os.environ['KYNVA_API_KEY']}"},
)
res.raise_for_status()

Rotate a key

  1. In the dashboard, click Rotate on the key you want to replace.
  2. The new key is shown once. Update your secrets store immediately.
  3. The old key keeps working for a 24-hour grace window. After that it returns UNAUTHORIZED.
Schedule a quarterly rotation. Kynva sends an email reminder at the 80-day mark for any key older than 90 days.

Store keys safely

  • Backend: load from your secret manager (AWS Secrets Manager, GCP Secret Manager, Doppler, 1Password Connect). Never commit to git.
  • Local dev: use a .env file ignored by git, or your shell’s keychain.
  • CI: provision as a masked secret in GitHub Actions / GitLab CI / etc.
If a key leaks, rotate immediately. The dashboard shows last-used timestamp and source IP — use both to audit blast radius.

Method 2 — JWT (end-user, short-lived)

If your product makes Kynva calls on behalf of an end user (e.g., a browser app where the user pays per render), use JWTs instead of API keys.

How it works

[Your frontend]  ──► [Your backend]  ──► [Kynva]
                          │                  │
                          │ mints JWT        │ verifies JWT
                          │ (signs with      │ via JWKS
                          │  your private    │
                          │  key)            │
  1. Configure your JWT issuer in Settings → Auth — issuer URL + JWKS endpoint.
  2. Your backend mints a JWT for the requesting user. Include:
    • sub — the user’s stable ID in your system
    • aud: "kynva"
    • exp — short. 5–15 minutes is typical.
  3. Pass the JWT as the bearer token.

Send the JWT

# Any authenticated endpoint works the same way — listing brands here;
# rendering uses POST /api/v1/facade/generate (see the quickstart).
curl https://api.kynva.ai/api/v1/facade/brands \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "X-API-Version: 2026-01-01"
import { SignJWT } from "jose";
import { randomUUID } from "node:crypto";

// On your backend — never ship the private key to clients
const privateKey = await loadPrivateKey(); // your secrets store

async function mintUserJwt(userId: string) {
  return await new SignJWT({})
    .setProtectedHeader({ alg: "RS256", kid: "kynva-2026-01" })
    .setIssuer("https://your-app.example.com")
    .setAudience("kynva")
    .setSubject(userId)
    .setIssuedAt()
    .setExpirationTime("10m")
    .sign(privateKey);
}

const jwt = await mintUserJwt("user_42");

// Any authenticated endpoint works the same way — rendering uses
// POST /api/v1/facade/generate (see the quickstart).
await fetch("https://api.kynva.ai/api/v1/facade/brands", {
  headers: {
    Authorization: `Bearer ${jwt}`,
    "X-API-Version": "2026-01-01",
  },
});
import os, uuid, time, jwt, requests

# On your backend — load your RSA private key from secrets
with open("/secrets/kynva-jwt-private.pem", "rb") as f:
    private_key = f.read()

def mint_user_jwt(user_id: str) -> str:
    return jwt.encode(
        {
            "iss": "https://your-app.example.com",
            "aud": "kynva",
            "sub": user_id,
            "iat": int(time.time()),
            "exp": int(time.time()) + 600,  # 10 minutes
        },
        private_key,
        algorithm="RS256",
        headers={"kid": "kynva-2026-01"},
    )

token = mint_user_jwt("user_42")

# Any authenticated endpoint works the same way — rendering uses
# POST /api/v1/facade/generate (see the quickstart).
res = requests.get(
    "https://api.kynva.ai/api/v1/facade/brands",
    headers={
        "Authorization": f"Bearer {token}",
        "X-API-Version": "2026-01-01",
    },
)
res.raise_for_status()

JWT requirements

ClaimRequiredNotes
issyesMust match the issuer URL configured in your workspace.
audyesMust be kynva.
subyesStable user identifier. Used for usage attribution and rate limits.
expyesMust be in the future and within 24h of iat. We recommend ≤ 15 min.
iatrecommendedIssued-at timestamp.
nbfoptionalIf set, must be ≤ now.
Tokens with missing or invalid claims are rejected with UNAUTHORIZED.

Verifying a request succeeded

A successful authenticated request includes:
HTTP/1.1 200 OK
X-Request-Id: req_2k4j8h9d
X-RateLimit-Remaining: 119
A failed authentication request returns:
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required",
    "request_id": "req_2k4j8h9d"
  }
}
See error codes for the full taxonomy.

Frequently asked

No. Legacy rf_* keys keep working indefinitely. New keys are issued with the kyn_ prefix. Nothing about the wire protocol changed.
Yes. In the dashboard, edit a key and add brand:brand_abc to its scope. The key will return FORBIDDEN if used against any other brand.
Yes. Same backend, same code, but renders made with a test key are watermarked and don’t consume credits.
Limits apply per workspace, not per key. See Rate limits for the per-category caps.