Authentication
Auth endpoints — send verification code, verify code, whoami, and agent login.
Auth endpoints for email verification, user profiles, and API key management. All auth routes are prefixed with /v1/auth.
POST /v1/auth/send-code
Send a 6-digit verification code to an email address. Rate limited to 5 codes per email per hour. Codes expire after 10 minutes.
Auth required: No
// Request
{
"email": "[email protected]"
}
// Response (200)
{
"message": "Verification code sent"
}POST /v1/auth/verify-code
Verify the code and authenticate. Sets a JWT cookie (token, httpOnly, 10-year maxAge — sessions don't expire). If the email has no account, one is created automatically along with a default team.
Auth required: No
// Request
{
"email": "[email protected]",
"code": "123456"
}
// Response (200 existing user, 201 new user)
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"email": "[email protected]",
"name": "User",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
},
"teams": [
{
"id": "uuid",
"name": "User's Team",
"slug": "user-abc12345",
"role": "owner",
"default_agent_key": "owl_agent_..."
}
],
"is_new_user": false
}Each team includes a default_agent_key field — the oldest non-deleted agent key for that team. Used by the MCP setup flow; omitted if no agent key exists.
POST /v1/auth/agent-login
Verify code and provision an agent API key in one step. Used by the CLI. Does not set a JWT cookie.
If the user belongs to multiple teams and team_id is not specified, returns 400 with the list of teams.
Auth required: No
// Request
{
"email": "[email protected]",
"code": "123456",
"team_id": "uuid" // optional if user has one team
}
// Response (201)
{
"api_key": "owl_agent_abc123...",
"team": {
"id": "uuid",
"name": "My Team",
"slug": "my-team"
}
}
// Error: multiple teams, no team_id specified (400)
{
"error": "Multiple teams found. Specify team_id.",
"teams": [
{ "id": "uuid1", "name": "Team A", "slug": "team-a", "role": "owner" },
{ "id": "uuid2", "name": "Team B", "slug": "team-b", "role": "member" }
]
}POST /v1/auth/default-agent-key
Lazy-create a default agent key for the team. If an agent key already exists for the team, returns that key's secret. Used by the MCP setup flow so agents can bootstrap access without manually creating a key.
Auth required: Yes (JWT only)
// Request
{
"team_id": "uuid"
}
// Response — existing key returned (200)
{
"secret": "owl_agent_abc123...",
"created": false
}
// Response — new key created (201)
{
"secret": "owl_agent_xyz789...",
"created": true
}POST /v1/auth/logout
Clear the JWT cookie.
Auth required: No
// Response (200)
{
"success": true
}GET /v1/auth/whoami
Return identity info for the current auth context. Works with both JWT and API key auth.
Auth required: Yes (JWT or API key)
// Response — API key auth
{
"type": "api_key",
"key_type": "agent",
"team": {
"id": "uuid",
"name": "My Team",
"slug": "my-team"
},
"permissions": ["events:read", "apps:read", "apps:write", "..."]
}
// Response — JWT auth
{
"type": "user",
"email": "[email protected]",
"teams": [
{ "id": "uuid", "name": "My Team", "slug": "my-team", "role": "owner" }
]
}GET /v1/auth/teams
List all teams the authenticated user belongs to.
Auth required: Yes (JWT only)
// Response (200)
{
"teams": [
{
"id": "uuid",
"name": "My Team",
"slug": "my-team",
"role": "owner"
}
]
}GET /v1/auth/me
Get the current user's profile and team memberships.
Auth required: Yes (JWT only)
// Response (200)
{
"user": {
"id": "uuid",
"email": "[email protected]",
"name": "User",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
},
"teams": [
{ "id": "uuid", "name": "My Team", "slug": "my-team", "role": "owner" }
]
}PATCH /v1/auth/me
Update the current user's profile.
Auth required: Yes (JWT only)
// Request
{
"name": "New Name"
}
// Response (200)
{
"user": {
"id": "uuid",
"email": "[email protected]",
"name": "New Name",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-15T12:00:00.000Z"
}
}GET /v1/auth/keys
List all API keys for teams the user belongs to.
Auth required: Yes (JWT only)
| Parameter | Type | Description |
|---|---|---|
team_id | string | Optional. Filter to keys from a specific team. |
// Response (200)
{
"api_keys": [
{
"id": "uuid",
"secret": "owl_client_abc1",
"key_type": "client",
"app_id": "uuid",
"team_id": "uuid",
"name": "iOS App Client Key",
"created_by": "uuid",
"created_by_email": "[email protected]",
"permissions": ["events:write"],
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z",
"last_used_at": "2024-01-15T10:00:00.000Z",
"expires_at": null,
"app_name": "iOS App"
}
]
}GET /v1/auth/keys/:id
Get a single API key by ID.
Auth required: Yes (JWT only)
// Response (200)
{
"api_key": {
"id": "uuid",
"secret": "owl_agent_xyz9",
"key_type": "agent",
"app_id": null,
"team_id": "uuid",
"name": "CLI Agent Key",
"created_by": "uuid",
"permissions": ["events:read", "apps:read", "apps:write", "..."],
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z",
"last_used_at": null,
"expires_at": null
}
}POST /v1/auth/keys
Create a new API key. The full key is returned once and cannot be retrieved again.
Auth required: Yes (JWT only, admin role required)
// Request
{
"name": "Production Agent Key",
"key_type": "agent", // "client", "agent", or "import"
"team_id": "uuid", // required for agent keys without app_id
"app_id": "uuid", // required for client keys
"permissions": ["events:read", "apps:read"], // optional, defaults applied per key type
"expires_in_days": 90 // optional, key never expires if omitted
}
// Response (201)
{
"api_key": {
"id": "uuid",
"secret": "owl_agent_full_key...",
"key_type": "agent",
"app_id": null,
"team_id": "uuid",
"name": "Production Agent Key",
"created_by": "uuid",
"permissions": ["events:read", "apps:read"],
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z",
"last_used_at": null,
"expires_at": "2024-04-01T00:00:00.000Z"
}
}Permission defaults
If permissions is omitted, defaults are applied:
| Key type | Default permissions |
|---|---|
| Client | ["events:write", "users:write"] |
| Agent | All agent permissions: events:read, funnels:read, funnels:write, apps:read, apps:write, projects:read, projects:write, metrics:read, metrics:write, audit_logs:read, users:write, integrations:read, integrations:write, jobs:read, jobs:write, issues:read, issues:write |
| Import | ["events:write", "users:write"] |
Client and import keys share the same permissions (events:write, users:write). Import keys require an app_id and are used with the bulk import endpoint. Agent keys with apps:write permission can create import keys.
PATCH /v1/auth/keys/:id
Update an API key's name or permissions.
Auth required: Yes (JWT only, admin role required)
// Request
{
"name": "Renamed Key",
"permissions": ["events:read", "apps:read"]
}
// Response (200)
{
"api_key": { ... }
}DELETE /v1/auth/keys/:id
Revoke (soft-delete) an API key.
Auth required: Yes (JWT only, admin role required)
// Response (200)
{
"deleted": true
}