Owlmetry
API Reference

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)

ParameterTypeDescription
team_idstringOptional. 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 typeDefault permissions
Client["events:write", "users:write"]
AgentAll 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
}

Ready to get started?

Connect your agent via MCP or CLI and start tracking.