Getting Started

Kiteworks API Authentication Flows

Before you can call the Kiteworks API, your application needs an access token — a short-lived credential that proves permission to act on a user's behalf. Kiteworks issues access tokens through two OAuth 2.0 flows. Choose the one that matches how your application is built and where it runs.

FlowBest forUser contextSecurity note
Authorization Code Web apps or desktop tools where a real user clicks "Sign in with Kiteworks" Yes — each token is tied to the user who signed in Industry-standard flow; add PKCE for extra protection against code theft
JWT Bearer Backend scripts, scheduled jobs, or automation that runs without a user present Yes — the token acts as a specific Kiteworks user you configure in advance Requires a private key stored securely on the server; never use in a browser or mobile app
Both flows require a registered client application. If you haven't created one yet, see Create a Custom Application to obtain your client_id, client_secret, and redirect URI.

Authorization Code Flow

Use this flow when a real person needs to sign in. Your application opens the Kiteworks login page in a browser, the user enters their credentials, and Kiteworks redirects them back to your app with a short-lived code. Your app then exchanges that code for an access token behind the scenes. Think of it as "Sign in with Kiteworks" — similar to how social login works with Google or GitHub. This is the right choice for most web apps, desktop clients, and mobile apps.

Never store client_id, client_secret, or access_token as constants in source code where others can access them. Use environment variables or a secrets manager.

Requirements

ParameterDescription
client_idThe unique system-generated ID of your registered client application.
client_secretThe secret that authenticates your client to the Kiteworks server.
redirect_uriThe URI your client listens on for the authorization result. For mobile clients or those that cannot redirect, use https://{kiteworks_server}/oauth_callback.php.
scopeThe set of API services your client needs access to. Consult your administrator for available scopes.
grant_typeMust be set to authorization_code.

Endpoints

PurposeEndpoint
Authorizationhttps://{kiteworks_server}/oauth/authorize
Tokenhttps://{kiteworks_server}/oauth/token

Sequence

  1. Client redirects the user to the authorization endpoint
    Your application redirects the user-agent (browser or web view) to /oauth/authorize with your client_id, redirect_uri, response_type=code, and requested scope.
  2. User authenticates and grants access
    The Kiteworks server presents a login page. The user authenticates and chooses whether to grant or deny the access request.
  3. Server redirects back with an authorization code
    On success, the server redirects to your redirect_uri with a code parameter containing the short-lived authorization code.
  4. Client exchanges the code for an access token
    Your application POSTs to /oauth/token with the authorization code, client credentials, and grant_type=authorization_code.
  5. Server returns the access token
    The server validates the credentials and authorization code and responds with an access_token (and optionally a refresh_token) in JSON format.

Step 1 — Authorization request

Send a GET request to the authorization endpoint. All parameters are passed as query string values.

ParameterRequiredDescription
client_idRequiredYour registered client application ID.
response_typeRequiredMust be code.
redirect_uriRequiredMust be URL-encoded and start with the URI registered for your client. For example, if registered as https://mydomain.com/oauth, you may pass https://mydomain.com/oauth/callback.
scopeRequiredSpace-separated list of requested API scopes. Must be a subset of the client's registered scopes. If blank, the registered scope is used.
stateOptionalAn opaque value passed through unchanged in the response. Use this to maintain state in your application and protect against CSRF.
mOptionalSet to 1 to display a mobile-friendly authorization page.
Authorization request
GET https://{kiteworks_server}/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &response_type=code
  &scope=YOUR_SCOPE
  &redirect_uri=https%3A%2F%2F{kiteworks_server}%2Foauth_callback.php
  HTTP/1.1

Successful response

The server redirects the user-agent (HTTP 302) to your redirect_uri with the authorization code:

302 Redirect
HTTP/1.1 302 Found
Location: https://{kiteworks_server}/oauth_callback.php?code=60cc146c8dced75e26e

Error response

For invalid client ID or redirect URI, an error is shown in the browser directly. For other errors, the server redirects to your redirect_uri with an error parameter:

Error codeCause
access_deniedThe user denied the permission request.
invalid_scopeThe requested scope is invalid.
invalid_requestA required parameter is missing, unsupported, or malformed.
unauthorized_clientThe client application is not authorized to use this flow.

Step 2 — Access token request

Exchange the authorization code from step 1 for an access token by POSTing to the token endpoint.

ParameterRequiredDescription
client_idRequiredYour registered client application ID.
client_secretRequiredYour client's secret phrase.
grant_typeRequiredMust be authorization_code.
redirect_uriRequiredThe same redirect URI used in step 1.
codeRequiredThe authorization code received in step 1.
install_tag_idOptionalA string uniquely identifying the device making the request.
install_nameOptionalA friendly name for the device making the request.
Token request
POST /oauth/token HTTP/1.1
Host: {kiteworks_server}
Content-Type: application/x-www-form-urlencoded

client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&grant_type=authorization_code
&code=60cc146c8dced75e26e
&redirect_uri=https%3A%2F%2F{kiteworks_server}%2Foauth_callback.php

Successful response

200 OK
{
  "access_token": "d932e1d32d89140163345d47fa97bfa60eeba1a5",
  "expires_in": "360000",
  "token_type": "bearer",
  "scope": "GET\/users\/* *\/files\/*",
  "refresh_token": "d7ce54d721e8das60943f3fc7cb159e4b11d0ee5"
}
FieldDescription
access_tokenThe token to include in API requests. Never store this in source code.
expires_inSeconds until the access token expires.
token_typeAlways bearer.
scopeThe scopes this token is valid for.
refresh_tokenPresent only if the client is configured to use refresh tokens. Use it to obtain a new access token without repeating the authorization flow.

Error response

Returns HTTP 400 Bad Request with a JSON error body:

Error codeCause
invalid_clientClient authentication failed — the client ID and/or secret is invalid.
invalid_requestA required parameter is missing, unsupported, or malformed.
unauthorized_clientThe client is not authorized to use this flow.

Python sample

The KWOAuthClient class below handles the full Authorization Code flow with PKCE. It caches the access token, refreshes it automatically when it expires, and only opens a browser window when re-authentication is required.

get_access_token.py
#!/usr/bin/env python
# File: python/get_access_token.py

import time
import requests
import webview
from urllib.parse import urlparse, parse_qs
import base64
import hashlib
import os

class KWOAuthClient:
    def __init__(self, base_url):
        self.authorization_endpoint = f"{base_url}/oauth/authorize"
        self.token_endpoint = f"{base_url}/oauth/token"
        self.redirect_uri = f"{base_url}/rest/callback.html"
        self._credential_manager = _CredentialManager()

    @staticmethod
    def _generate_pkce_pair():
        code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('utf-8')
        code_challenge = base64.urlsafe_b64encode(
            hashlib.sha256(code_verifier.encode('utf-8')).digest()
        ).rstrip(b'=').decode('utf-8')
        return code_verifier, code_challenge

    def _is_token_expired(self):
        token_data = self._credential_manager.retrieve_saved_access_token()
        if token_data is None:
            return True
        return int(time.time()) >= token_data['expiry']

    def _refresh_access_token(self):
        refresh_token = self._credential_manager.retrieve_saved_refresh_token()
        if not refresh_token:
            raise ValueError("No refresh token available.")
        data = {
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": self._credential_manager.get_client_id(),
            "client_secret": self._credential_manager.get_client_secret()
        }
        response = requests.post(self.token_endpoint, data=data)
        response.raise_for_status()
        return response.json()

    def _exchange_code_for_tokens(self, code, code_verifier):
        data = {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": self.redirect_uri,
            "client_id": self._credential_manager.get_client_id(),
            "client_secret": self._credential_manager.get_client_secret(),
            "code_verifier": code_verifier
        }
        response = requests.post(self.token_endpoint, data=data)
        response.raise_for_status()
        return response.json()

    def _save_tokens(self, tokens):
        expires_at = int(time.time()) + tokens.get('expires_in', 3600)
        self._credential_manager.save_tokens(
            tokens['access_token'],
            tokens.get('refresh_token'),
            expires_at
        )

    def _poll_url(self, window, timeout=60):
        start = time.time()
        while True:
            current_url = window.get_current_url()
            if current_url.startswith(self.redirect_uri):
                parsed = urlparse(current_url)
                qs = parse_qs(parsed.query)
                if "code" in qs:
                    self.auth_code = qs["code"][0]
                    window.destroy()
                    break
            if (time.time() - start) > timeout:
                print("Timeout waiting for redirect.")
                window.destroy()
                break
            time.sleep(0.5)

    def _start_oauth_flow(self, code_challenge):
        auth_url = (
            f"{self.authorization_endpoint}"
            f"?client_id={self._credential_manager.client_id}"
            f"&redirect_uri={self.redirect_uri}"
            f"&response_type=code"
            f"&code_challenge={code_challenge}"
            f"&code_challenge_method=S256"
        )
        window = webview.create_window("Login", auth_url)
        webview.start(lambda: self._poll_url(window))
        return self.auth_code

    def get_access_token(self):
        # 1) Return cached token if still valid
        if not self._is_token_expired():
            saved_token = self._credential_manager.retrieve_saved_access_token()
            return saved_token['access_token']

        # 2) Refresh if a refresh token is available
        if self._credential_manager.retrieve_saved_refresh_token() is not None:
            try:
                print("Refreshing access token...")
                tokens = self._refresh_access_token()
                self._save_tokens(tokens)
                return tokens["access_token"]
            except Exception as e:
                print("Failed to refresh token:", e)

        # 3) Full OAuth flow with PKCE
        code_verifier, code_challenge = self._generate_pkce_pair()
        print("Reauthentication required. Opening browser...")
        code = self._start_oauth_flow(code_challenge)
        if not code:
            raise RuntimeError("No authorization code obtained (timeout?).")
        tokens = self._exchange_code_for_tokens(code, code_verifier)
        self._save_tokens(tokens)
        return tokens["access_token"]


class _CredentialManager:
    """
    In-memory credential store for demonstration purposes.
    In production, use a secure secrets manager.
    """
    def __init__(self):
        self.token_data = None
        self.client_id = input("Enter your Client ID: ")
        self.client_secret = input("Enter your Client Secret: ")

    def get_client_id(self):
        return self.client_id

    def get_client_secret(self):
        return self.client_secret

    def save_tokens(self, access_token, refresh_token, expiry):
        self.token_data = {
            'access_token': access_token,
            'refresh_token': refresh_token,
            'expiry': expiry
        }

    def retrieve_saved_access_token(self):
        return self.token_data

    def retrieve_saved_refresh_token(self):
        if self.token_data and 'refresh_token' in self.token_data:
            return self.token_data['refresh_token']
        return None


if __name__ == "__main__":
    base_url = input("Enter Base URL for Kiteworks Instance: ")
    client = KWOAuthClient(base_url)
    token = client.get_access_token()
    print("Got access token:", token)
The _CredentialManager class uses in-memory storage for demonstration purposes. In production, replace it with a secure credential store such as your operating system's keychain, AWS Secrets Manager, or HashiCorp Vault.

JWT Bearer Flow

Use this flow when there is no user present — for example, a nightly sync script, a backend data pipeline, or any automation that runs on a schedule. Instead of opening a browser, your server generates a cryptographically signed token (a JWT) that proves its identity, then exchanges it for a Kiteworks access token entirely over HTTPS. No login page, no redirect. The resulting access token acts on behalf of a specific Kiteworks user account you designate when you register the application.

This flow is intended for clients operating in secure server-side environments only. Because it allows user impersonation, using it for publicly accessible clients is a security risk and should be avoided.

About JSON Web Tokens

A JWT (JSON Web Token) is a short string your application generates and signs with its private key. It looks like three base64 chunks joined by dots: xxxxx.yyyyy.zzzzz. Kiteworks decodes it, checks the signature against the public key you registered, and — if everything matches — issues an access token. You never send your private key over the network; only the signed token travels to Kiteworks. The format is defined by RFC 7519 and consists of three parts:

PartContents
Header Token type (JWT) and signing algorithm (e.g. RS256).
Payload Claims — statements about the user and token metadata. Includes registered claims (iss, sub, aud, exp, etc.) and any additional claims.
Signature Created by signing the encoded header and payload with a private key. Verifies the token's integrity and authenticity.

Application setup

Before using this flow, a Kiteworks administrator must enable JWT on your custom application in the Admin console. The values configured here must exactly match the claims embedded in every JWT your application generates — a mismatch causes the token request to be rejected.

FieldDescription
Subject (UID Attribute)Which JWT claim identifies the Kiteworks user to act as. Set this to sub and populate sub with the user's email address in each JWT you generate.
AudienceA string identifying who the JWT is intended for — usually the same Kiteworks instance URL. Whatever value you set here must appear in the aud claim of every JWT.
IssuerA string identifying who created the JWT — typically your Kiteworks instance URL (e.g. https://your.kiteworks.com). Whatever value you set here must appear in the iss claim of every JWT.
AlgorithmThe cryptographic algorithm used to sign the JWT. Use RS256 (RSA + SHA-256) unless your administrator specifies otherwise.
Public KeyThe public half of the RSA key pair used to sign JWTs. Paste a PEM-encoded public key here, or click Generate public-private key pair to have Kiteworks generate one — the public key is filled in automatically and the private key is shown once for you to save. Kiteworks uses this key to verify the signature on incoming JWTs.

Step 1 — Generate a JWT assertion

If you don't have an RSA key pair yet, generate one now. Run the following commands once — keep private_key.pem on your server and register the contents of public_key.pem in the Admin console:

Generate RSA key pair
# Generate a 2048-bit RSA private key
openssl genrsa -out private_key.pem 2048

# Extract the public key
openssl rsa -in private_key.pem -pubout -out public_key.pem

Your application then builds and signs the JWT for each token request. The JWT header must specify {"alg": "RS256", "typ": "JWT"}. The payload must include the following claims:

ClaimDescription
issIssuer — must match the Issuer configured on the application.
subSubject — the email address of the Kiteworks user to act as.
audAudience — must match the Audience configured on the application.
iatIssued At — Unix timestamp of when the JWT was created.
nbfNot Before — Unix timestamp before which the JWT must not be accepted.
expExpiration Time — Unix timestamp after which the JWT expires. Set to a short duration, e.g. 5 minutes (300 seconds) from iat.
jtiJWT ID — a unique identifier (e.g. a UUID) to prevent replay attacks.

Step 2 — Request an access token

POST the signed JWT to the token endpoint. A new JWT must be generated for each access token request — the JWT assertion itself is short-lived and cannot be reused.

ParameterRequiredDescription
client_idRequiredYour registered client application ID.
client_secretRequiredYour client's secret phrase.
grant_typeRequiredMust be urn:ietf:params:oauth:grant-type:jwt-bearer.
assertionRequiredThe complete signed JWT token generated in step 1.
scopeOptionalRequested API scopes. Must be a subset of the client's registered scopes.
install_tag_idOptionalA string uniquely identifying the device making the request.
install_nameOptionalA friendly name for the device making the request.
JWT token request
POST /oauth/token HTTP/1.1
Host: kiteworks_server
Content-Type: application/x-www-form-urlencoded

client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGci...

Successful response

200 OK
{
  "access_token": "054915e674bc35fa7fff1f499044e964d3a5d61b",
  "expires_in": 3600,
  "token_type": "bearer",
  "scope": "*/files/* */folders/*"
}
Unlike the Authorization Code flow, the JWT flow does not issue a refresh token. When the access token expires, you must generate a new JWT assertion and request a new access token.

Python sample

The OAuthJWTAssertionClient class handles JWT generation and token requests. It signs the JWT with an RSA private key and exchanges it for an access token at the Kiteworks token endpoint.

jwt_assertion.py
import time
import uuid
import jwt
import requests
from cryptography.hazmat.primitives import serialization

class OAuthJWTAssertionClient:
    """
    OAuth 2.0 JWT Assertion Flow with RSA signatures.

    The Authorization Server expects:
      - client_id
      - client_secret
      - grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
      - assertion  = <signed JWT>

    JWT claims: iss, sub, aud, iat, nbf, exp, jti
    """

    def __init__(self, issuer, subject, audience, private_key,
                 token_endpoint, client_id, client_secret, algorithm="RS256"):
        """
        :param issuer:         The issuer of the JWT (e.g. the Kiteworks domain URL).
        :param subject:        The Kiteworks user account the client acts as.
        :param audience:       The JWT audience — must match the application configuration.
        :param private_key:    RSA private key (PEM) used to sign the JWT.
        :param token_endpoint: URL of the Kiteworks OAuth token endpoint.
        :param client_id:      Registered client application ID.
        :param client_secret:  Registered client secret.
        :param algorithm:      JWT signing algorithm (default: RS256).
        """
        self.issuer = issuer
        self.subject = subject
        self.audience = audience
        self.private_key = private_key
        self.token_endpoint = token_endpoint
        self.client_id = client_id
        self.client_secret = client_secret
        self.algorithm = algorithm

    def create_jwt_assertion(self, validity_seconds=300):
        """
        Create and sign a JWT assertion.
        :param validity_seconds: JWT lifetime in seconds (default: 300 / 5 minutes).
        """
        now = int(time.time())
        payload = {
            "iss": self.issuer,
            "sub": self.subject,
            "aud": self.audience,
            "iat": now,
            "nbf": now,
            "exp": now + validity_seconds,
            "jti": str(uuid.uuid4())
        }
        return jwt.encode(payload, self.private_key, algorithm=self.algorithm)

    def get_access_token(self, scope=None, validity_seconds=300):
        """
        Request an access token using the JWT Bearer assertion.
        :param scope:            Optional scope string.
        :param validity_seconds: JWT lifetime in seconds.
        :return: Access token string.
        """
        assertion = self.create_jwt_assertion(validity_seconds=validity_seconds)
        data = {
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "assertion": assertion,
        }
        if scope:
            data["scope"] = scope

        response = requests.post(self.token_endpoint, data=data)
        response.raise_for_status()

        token_response = response.json()
        access_token = token_response.get("access_token")
        if not access_token:
            raise ValueError("No access_token in response.")
        return access_token
jwt_assertion.py — __main__
if __name__ == "__main__":
    from cryptography.hazmat.primitives import serialization

    host_name = input("Enter Kiteworks instance domain (e.g. https://your.domain): ")
    token_endpoint = f"{host_name}/oauth/token"

    private_key_path = input("Enter private key file path (.pem): ")
    with open(private_key_path, "rb") as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None
        )

    client = OAuthJWTAssertionClient(
        issuer=host_name,
        subject=input("Enter Kiteworks username (email): "),
        audience=input("Enter JWT audience: "),
        private_key=private_key,
        token_endpoint=token_endpoint,
        client_id=input("Enter client ID: "),
        client_secret=input("Enter client secret: ")
    )

    access_token = client.get_access_token(validity_seconds=300)
    print("Access token:", access_token)

Frequently Asked Questions

Which OAuth 2.0 flow should I use with the Kiteworks API?

Ask yourself: is a real person clicking "Sign in"? If yes, use Authorization Code with PKCE — it redirects the user to a Kiteworks login page and is the right choice for web apps, desktop clients, and mobile apps. If your code runs unattended (a cron job, a pipeline, a backend service), use the JWT Bearer flow — it requires no browser and no user interaction, but it does require you to store a private key securely on your server.

How long does a Kiteworks API access token last?

Access token lifetime is configured per custom application in the Kiteworks Admin console. A common default is 3600 seconds (one hour). Your application should refresh tokens before they expire to avoid interruptions. The JWT Bearer flow allows you to request a token with a specific validity window up to the configured maximum.

Is PKCE required for the Authorization Code flow?

PKCE (Proof Key for Code Exchange) is strongly recommended and required for public clients such as mobile apps and single-page applications. It prevents authorization code interception attacks by binding the token request to the original authorization request with a cryptographic challenge.

Where do I get the keys for the JWT Bearer flow?

You generate the key pair yourself using openssl (see the key generation command in Step 1). Register the public key in the Kiteworks Admin console under your custom application — this is what Kiteworks uses to verify incoming JWTs. Keep the private key on your server only; never share it or commit it to source control. When your application makes a token request, it signs the JWT with the private key. Kiteworks checks that signature against the public key it already has on file.

What OAuth scopes does the Kiteworks API use?

OAuth scopes control which API operations your access token may perform. Scopes are configured on your custom application in the Admin console. Common scopes include read and write access to files, folders, mail, and user data. Request only the scopes your integration actually needs.

Next Steps

With an access token in hand, you're ready to start calling the API: