Connect your application to Logfire with OAuth
Let customers connect their Logfire organization to your application without copying an API key. OAuth 2.0 is an authorization protocol: the customer approves specific permissions, and your application receives tokens to act within that approval.
Use an OAuth app for a partner integration that connects to customers’ Logfire resources. For automation within your own organization, you can use an API key. For connecting an existing AI tool, follow the MCP server guide.
We are happy to help you integrate with Logfire. Email engineering@pydantic.dev to discuss your integration or get help with setup.
You need permission to manage your organization’s settings. Register the app in your own organization. Each customer separately approves access to their organization and, when applicable, a project. An administrator of the customer’s organization must approve an app registered through organization settings.
- Open OAuth apps in your default organization.
- Select New OAuth app.
- Enter an Application Name that customers will recognize on the consent screen.
- Choose an Application Type:
- Confidential: your server can keep a client secret private. Use this for a partner’s hosted backend.
- Public: the application cannot keep a secret, such as a browser or mobile app.
- Add your Redirect URIs: the callback URLs where your application receives the authorization result.
Use HTTPS for a hosted callback. Loopback HTTP URLs, such as
http://127.0.0.1:8000/callback, are supported for local development. - Select the Allowed Scopes your integration needs. A scope names a permission, such as
project:read. - Create the app and save its Client ID. For a confidential app, also save the Client Secret.
If OAuth apps is missing, check your organization permissions and contact us about app registration access.
Use the authorization-code flow with Proof Key for Code Exchange (PKCE).
PKCE binds the authorization code to a random value generated by your application, so intercepting the code alone is insufficient to exchange it.
Logfire requires the S256 method for both public and confidential apps.
Use one data region throughout registration, authorization, token exchange, and API calls. Register an app separately in each region you support.
| Setting | US | EU |
|---|---|---|
| Authorization URL | https://logfire-us.pydantic.dev/api/oauth/authorize | https://logfire-eu.pydantic.dev/api/oauth/authorize |
| Token URL | https://logfire-us.pydantic.dev/api/oauth/token | https://logfire-eu.pydantic.dev/api/oauth/token |
| Public API base URL | https://api-us.pydantic.dev/api/v1 | https://api-eu.pydantic.dev/api/v1 |
| API reference | US API | EU API |
Configure your OAuth library with your registered client ID, callback URL, and these settings:
| Setting | Value |
|---|---|
| Response type | code |
| Grant type | authorization_code |
| PKCE challenge method | S256 |
| Confidential client authentication | client_secret_basic |
| Public client authentication | none |
| Scopes | Space-separated permissions selected when registering your app |
Choose a language tab below. Each example runs from your server or a local terminal. The TypeScript examples target Node.js, not browser code: confidential-client credentials must stay on your server.
- curl: install
curl, OpenSSL, andjq. - Python: save a Python example as
example.pyand runuv run --with httpx2 example.py. - TypeScript: save a TypeScript example as
example.mtsand runnode example.mtswith Node.js 24 or later. No packages are required.
Set the environment variables shown before each tab group, replacing placeholder values with your own. The examples print their results for manual verification. In your application, store tokens securely instead of logging them. They demonstrate the HTTP requests; your application still needs the callback handler and session storage described below.
When the customer selects Connect to Logfire in your application:
- Generate a fresh PKCE verifier and its
S256challenge. - Generate a random
statevalue and bind it, along with the verifier, to the customer’s session in your application. - Redirect the browser to the authorization URL with the parameters below.
- The customer signs in to Logfire, selects the organization and any project restriction, and approves the permissions.
- Logfire redirects to your callback with
codeandstate, or an OAuth error. - Verify the returned
stateagainst the saved value before exchanging the code. Reject a missing or mismatched value.
| Authorization parameter | Value |
|---|---|
response_type | code |
client_id | The app’s client ID |
redirect_uri | Your registered callback URL |
scope | For example, project:read |
state | The random value bound to this connection attempt |
code_challenge | Base64url-encoded SHA-256 digest of the verifier, without padding |
code_challenge_method | S256 |
The verifier must contain 43 to 128 characters from A-Z, a-z, 0-9, -, ., _, and ~.
Keep it private until the token exchange. Use your OAuth library’s PKCE and state handling when available.
For a manual integration check, these examples generate fresh values and an authorization URL. Replace the client ID and callback URL with your registered values.
export LOGFIRE_BASE_URL='https://logfire-us.pydantic.dev'
export CLIENT_ID='YOUR_CLIENT_ID'
export REDIRECT_URI='https://your-app.example.com/oauth/logfire/callback'
export CODE_VERIFIER="$(openssl rand -hex 32)"
export STATE="$(openssl rand -hex 32)"
CODE_CHALLENGE="$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
jq -nr \
--arg base "$LOGFIRE_BASE_URL" \
--arg client "$CLIENT_ID" \
--arg redirect "$REDIRECT_URI" \
--arg state "$STATE" \
--arg challenge "$CODE_CHALLENGE" \
'$base + "/api/oauth/authorize?" + ({
response_type: "code", client_id: $client, redirect_uri: $redirect,
scope: "project:read", state: $state,
code_challenge: $challenge, code_challenge_method: "S256"
} | to_entries | map(.key + "=" + (.value | @uri)) | join("&"))'
import base64
import hashlib
import json
import os
import secrets
import sys
from urllib.parse import urlencode
base_url = os.environ['LOGFIRE_BASE_URL']
client_id = os.environ['CLIENT_ID']
redirect_uri = os.environ['REDIRECT_URI']
code_verifier = secrets.token_urlsafe(32)
state = secrets.token_urlsafe(32)
code_challenge = (
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode('ascii')).digest()).rstrip(b'=').decode('ascii')
)
query = urlencode(
{
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri,
'scope': 'project:read',
'state': state,
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
}
)
sys.stdout.write(
json.dumps(
{
'authorization_url': f'{base_url}/api/oauth/authorize?{query}',
'code_verifier': code_verifier,
'state': state,
},
indent=2,
)
+ '\n'
)
import { createHash, randomBytes } from "node:crypto";
const { LOGFIRE_BASE_URL, CLIENT_ID, REDIRECT_URI } = process.env;
if (!LOGFIRE_BASE_URL || !CLIENT_ID || !REDIRECT_URI) {
throw new Error("Set LOGFIRE_BASE_URL, CLIENT_ID, and REDIRECT_URI.");
}
const codeVerifier = randomBytes(32).toString("base64url");
const state = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const authorizationUrl = new URL("/api/oauth/authorize", LOGFIRE_BASE_URL);
authorizationUrl.search = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: "project:read",
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
}).toString();
console.log(
JSON.stringify(
{
authorization_url: authorizationUrl.toString(),
code_verifier: codeVerifier,
state,
},
null,
2,
),
);
The curl tab keeps CODE_VERIFIER and STATE in the current shell. The Python and TypeScript tabs return them as code_verifier and state in JSON.
Save both values for this connection attempt. For a manual check with Python or TypeScript, set CODE_VERIFIER to the returned verifier before running the token-exchange example.
Open the resulting URL in a browser. Your callback handler must perform the state check described above; generating an authorization URL does not implement that check for you.
Request only the permissions the integration needs. Logfire can grant fewer scopes than requested, so read the token response’s scope field.
Selecting a scope during app registration does not grant access to customer data by itself.
For a confidential app, send a form-encoded request from your server.
These examples use HTTP Basic authentication for the client credentials.
Set AUTHORIZATION_CODE only after your callback handler has validated the returned state:
export LOGFIRE_BASE_URL='https://logfire-us.pydantic.dev'
export CLIENT_ID='YOUR_CLIENT_ID'
export CLIENT_SECRET='YOUR_CLIENT_SECRET'
export REDIRECT_URI='https://your-app.example.com/oauth/logfire/callback'
export AUTHORIZATION_CODE='CODE_FROM_YOUR_VALIDATED_CALLBACK'
: "${CODE_VERIFIER:?Set the verifier saved for this authorization attempt}"
curl --fail-with-body --silent --show-error \
--user "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode "code=$AUTHORIZATION_CODE" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "code_verifier=$CODE_VERIFIER" \
"$LOGFIRE_BASE_URL/api/oauth/token"
import asyncio
import json
import os
import sys
import httpx2
base_url = os.environ['LOGFIRE_BASE_URL']
client_id = os.environ['CLIENT_ID']
client_secret = os.environ['CLIENT_SECRET']
redirect_uri = os.environ['REDIRECT_URI']
code = os.environ['AUTHORIZATION_CODE']
code_verifier = os.environ['CODE_VERIFIER']
async def main() -> None:
async with httpx2.AsyncClient(timeout=30.0) as client:
response = await client.post(
f'{base_url}/api/oauth/token',
auth=(client_id, client_secret),
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': redirect_uri,
'code_verifier': code_verifier,
},
)
response.raise_for_status()
sys.stdout.write(json.dumps(response.json(), indent=2) + '\n')
asyncio.run(main())
const {
LOGFIRE_BASE_URL,
CLIENT_ID,
CLIENT_SECRET,
REDIRECT_URI,
AUTHORIZATION_CODE,
CODE_VERIFIER,
} = process.env;
if (
!LOGFIRE_BASE_URL ||
!CLIENT_ID ||
!CLIENT_SECRET ||
!REDIRECT_URI ||
!AUTHORIZATION_CODE ||
!CODE_VERIFIER
) {
throw new Error("Set the token-exchange environment variables shown above.");
}
const credentials = Buffer.from(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64");
const response = await fetch(new URL("/api/oauth/token", LOGFIRE_BASE_URL), {
method: "POST",
headers: { Authorization: "Basic " + credentials },
body: new URLSearchParams({
grant_type: "authorization_code",
code: AUTHORIZATION_CODE,
redirect_uri: REDIRECT_URI,
code_verifier: CODE_VERIFIER,
}),
redirect: "error",
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error("Token exchange failed (HTTP " + response.status + ").");
}
console.log(JSON.stringify(await response.json(), null, 2));
Replace the values with your registered credentials and the values from this authorization attempt.
Use the same redirect_uri as in the authorization request. An authorization code can be exchanged only once.
For a public app, remove client-secret configuration and the client-authentication argument or header.
Send client_id in the form body instead: --data-urlencode "client_id=$CLIENT_ID" in curl, "client_id": client_id in Python, or client_id: CLIENT_ID in TypeScript.
In the TypeScript example, also remove CLIENT_SECRET from the required-variable check. PKCE is still required.
Do not combine HTTP Basic authentication with client credentials in the form body.
The successful JSON response includes:
| Field | Meaning |
|---|---|
access_token | Credential to send as a bearer token in API requests |
token_type | Bearer |
expires_in | Access-token lifetime in seconds |
refresh_token | Credential to obtain replacement tokens without repeating consent |
scope | The granted, space-separated scopes |
Keep access and refresh tokens private. Use expires_in to schedule renewal instead of assuming a fixed lifetime.
With project:read granted, list the projects visible to this token:
export LOGFIRE_API_BASE_URL='https://api-us.pydantic.dev/api/v1'
export ACCESS_TOKEN='ACCESS_TOKEN_FROM_THE_TOKEN_RESPONSE'
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $ACCESS_TOKEN" \
"$LOGFIRE_API_BASE_URL/projects/"
import asyncio
import json
import os
import sys
import httpx2
api_base_url = os.environ['LOGFIRE_API_BASE_URL']
access_token = os.environ['ACCESS_TOKEN']
async def main() -> None:
async with httpx2.AsyncClient(timeout=30.0) as client:
response = await client.get(
f'{api_base_url}/projects/',
headers={'Authorization': f'Bearer {access_token}'},
)
response.raise_for_status()
sys.stdout.write(json.dumps(response.json(), indent=2) + '\n')
asyncio.run(main())
const { LOGFIRE_API_BASE_URL, ACCESS_TOKEN } = process.env;
if (!LOGFIRE_API_BASE_URL || !ACCESS_TOKEN) {
throw new Error("Set LOGFIRE_API_BASE_URL and ACCESS_TOKEN.");
}
const response = await fetch(LOGFIRE_API_BASE_URL + "/projects/", {
headers: { Authorization: "Bearer " + ACCESS_TOKEN },
redirect: "error",
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error("Listing projects failed (HTTP " + response.status + ").");
}
console.log(JSON.stringify(await response.json(), null, 2));
For an EU connection, use https://api-eu.pydantic.dev/api/v1.
A successful request returns HTTP 200 and a JSON list of projects within the token’s permitted organization and project restrictions.
Check that the returned projects belong to the customer who completed consent.
Use the token URL from the same region as the original grant:
export LOGFIRE_BASE_URL='https://logfire-us.pydantic.dev'
export CLIENT_ID='YOUR_CLIENT_ID'
export CLIENT_SECRET='YOUR_CLIENT_SECRET'
export REFRESH_TOKEN='LATEST_REFRESH_TOKEN_FOR_THIS_CUSTOMER'
curl --fail-with-body --silent --show-error \
--user "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
"$LOGFIRE_BASE_URL/api/oauth/token"
import asyncio
import json
import os
import sys
import httpx2
base_url = os.environ['LOGFIRE_BASE_URL']
client_id = os.environ['CLIENT_ID']
client_secret = os.environ['CLIENT_SECRET']
refresh_token = os.environ['REFRESH_TOKEN']
async def main() -> None:
async with httpx2.AsyncClient(timeout=30.0) as client:
response = await client.post(
f'{base_url}/api/oauth/token',
auth=(client_id, client_secret),
data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
},
)
response.raise_for_status()
sys.stdout.write(json.dumps(response.json(), indent=2) + '\n')
asyncio.run(main())
const { LOGFIRE_BASE_URL, CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN } = process.env;
if (!LOGFIRE_BASE_URL || !CLIENT_ID || !CLIENT_SECRET || !REFRESH_TOKEN) {
throw new Error("Set LOGFIRE_BASE_URL, CLIENT_ID, CLIENT_SECRET, and REFRESH_TOKEN.");
}
const credentials = Buffer.from(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64");
const response = await fetch(new URL("/api/oauth/token", LOGFIRE_BASE_URL), {
method: "POST",
headers: { Authorization: "Basic " + credentials },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: REFRESH_TOKEN,
}),
redirect: "error",
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error("Token refresh failed (HTTP " + response.status + ").");
}
console.log(JSON.stringify(await response.json(), null, 2));
For a public app, use the same form-body client_id and omit client-secret configuration and authentication, as described for the code exchange.
Refresh tokens rotate: save the replacement refresh_token together with the new access token.
Coordinate refreshes for each connection so workers do not independently keep using an older refresh token.
If Logfire returns invalid_grant, ask the customer to reconnect instead of repeatedly retrying the same credential.
Open OAuth apps in the app owner’s organization settings to view the app’s details, manage redirect URLs and scopes, or manage confidential-client secrets. When rotating a secret, create a replacement, update your server to use it, and then revoke the old secret.
A customer can revoke an individual OAuth session from their account’s Security settings. Your integration must handle a revoked grant by asking the customer to reconnect.
| Symptom | Check |
|---|---|
| You cannot find OAuth Apps | Open OAuth apps in your default organization and check your organization permissions. |
invalid_client | Check the region, client ID, and active client secret. Public apps send a client ID without a secret. |
Authorization fails or returns access_denied | The customer must be an administrator of the selected organization. Check for declined consent and a callback URL that does not match registration. |
invalid_grant during exchange or refresh | Check the original PKCE verifier and callback URL. An expired or already-used code, or an invalid refresh token, requires a new authorization attempt. |
Use the regional API reference above to choose further endpoints and their required scopes.