Skip to content

Apps acting for people (authorization code + PKCE)

Use the authorization code flow with PKCE when your app acts for a person: a command-line tool, a desktop app, a single-page app or a server-side integration that people sign in to. The app can then do what that person may do, within the scopes they approve.

In Admin → API → Clients, select New API client and choose:

  • Public app for apps that cannot keep a secret (command-line, desktop, single-page apps). PKCE protects the flow; there is no secret.
  • Confidential app for server-side apps. It also gets a client secret (shown once, rotated like a machine client’s) and authenticates to the token endpoint with HTTP Basic.

Enter its Redirect URIs (one per line). They must match exactly: https://… addresses, or loopback addresses (http://127.0.0.1, http://localhost, http://[::1]) on any port for local tools. Public apps may also use a private-use scheme such as com.example.tool:/callback. Wildcards and fragments are not allowed.

An app registered by your organization can only be used with your organization.

  • Their role holds Connect apps to the API on my behalf. By default reviewers, approvers and admins have it; admins change it in Admin → AI tools (MCP) → Who can connect.
  • They turned on Settings → Connected apps → Allow apps and AI tools to act for me. Admins can’t turn it on for someone else; turning it off disconnects all their apps at once.
  1. Create a PKCE pair and a state. Only S256 is accepted, and state is required.

    const b64url = (bytes: ArrayBuffer | Uint8Array) =>
    btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
    const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)))
    const challenge = b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)))
    const state = b64url(crypto.getRandomValues(new Uint8Array(16)))
  2. Send the person to the authorize endpoint.

    const authorize = new URL("https://api.tradecrafthub.ai/oauth/authorize")
    authorize.search = new URLSearchParams({
    response_type: "code",
    client_id: CLIENT_ID,
    redirect_uri: "http://127.0.0.1:8765/callback",
    scope: "assessments:read questions:write assessments:review",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
    resource: "https://api.tradecrafthub.ai",
    }).toString()
  3. The organization. For an app your organization registered, the organization is fixed. Otherwise Tradecraft asks the person to type their organization’s address (it never lists organizations), or takes it from an organization=<address> parameter. People in several organizations connect once per organization and get one token per organization; the consent page offers Use a different organization.

  4. Consent on the organization’s own address. The person signs in there if needed and sees your app, the organization’s name, where they will be sent back and the scopes in plain words. Only scopes their role can grant are offered. Finalize questionnaires and Read your organization’s audit log also need a sign-in in the last 12 hours. Consent is remembered: next time the screen is skipped unless your app asks for more.

  5. The callback. Tradecraft redirects to your redirect_uri with code, state and iss=https://api.tradecrafthub.ai. Check that state is yours and iss is that issuer. If the person cancels, you get error=access_denied.

  6. Exchange the code within 60 seconds; a code works once.

    Terminal window
    curl -s https://api.tradecrafthub.ai/oauth/token \
    -d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER" \
    -d redirect_uri=http://127.0.0.1:8765/callback -d client_id="$CLIENT_ID" \
    -d resource=https://api.tradecrafthub.ai

    A confidential app adds -u "$CLIENT_ID:$CLIENT_SECRET". The response has an access_token (one hour), a refresh_token and the granted scope.

const res = await fetch("https://api.tradecrafthub.ai/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: stored.refreshToken, client_id: CLIENT_ID }),
})
const next = (await res.json()) as { access_token: string; refresh_token: string; expires_in: number }
// Store next.refresh_token now: the one you sent no longer works.
  • Refresh tokens rotate: every refresh returns a new one. Using an old one again revokes the whole chain, and the person has to connect again.
  • They expire after 30 days without use and 90 days after consent, whatever the use. Then the person consents again.
  • A refresh can narrow the scopes (scope=…), never widen them. For more scopes, send the person through consent again.
Terminal window
curl -s https://api.tradecrafthub.ai/oauth/revoke -d token="$REFRESH_TOKEN" -d client_id="$CLIENT_ID"

Revoking a refresh token revokes its chain; revoking an access token revokes that token. The answer is always 200. People see and revoke their connections in Settings → Connected apps; admins see everyone’s in Admin → API → Connected apps.

A person’s token can never do more than the person may do now: every request checks the token’s scopes and the person’s current role. If their role is lowered, the next request is limited accordingly; if they are removed or deactivated, their tokens are revoked at once.

These operations are for people only; machine clients get 403:

  • POST /v1/assessments/{id}/review-complete — Send to Finalize (scope assessments:review, reviewers and up)
  • POST /v1/assessments/{id}/finalize — Finalize (scope assessments:finalize, approvers and admins)

Privacy preference center

When you visit tradecrafthub.ai, we store or read a small number of cookies in your browser. Strictly necessary cookies make the website and the Tradecraft app work. Analytics cookies are used only if you allow them. Open a category to learn more, and see our privacy policy for how we handle the information you enter on this site.

Manage consent preferences

  • Always active