{
  "openapi": "3.0.3",
  "info": {
    "title": "Focus Pocus API",
    "description": "Backend service powering the achievements and mascot features in the epilot 360 portal.\nTracks user activity, evaluates achievement criteria, awards XP, and exposes the\nunified achievements feed and active challenge state.\n",
    "version": "0.2.0",
    "contact": {
      "name": "epilot",
      "email": "info@epilot.cloud",
      "url": "https://epilot.cloud"
    }
  },
  "servers": [
    {
      "url": "https://focus-pocus.dev.sls.epilot.io"
    },
    {
      "url": "https://focus-pocus.staging.sls.epilot.io",
      "description": "staging"
    },
    {
      "url": "https://focus-pocus.sls.epilot.io",
      "description": "prod"
    }
  ],
  "security": [
    {
      "EpilotAuth": []
    }
  ],
  "paths": {
    "/v1/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "getHealth",
        "description": "Liveness probe for the Focus Pocus API. Requires a valid EpilotAuth token.",
        "tags": [
          "Health"
        ],
        "responses": {
          "200": {
            "description": "Service is healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/achievements": {
      "get": {
        "operationId": "listAchievements",
        "summary": "listAchievements",
        "description": "Unified feed for the authenticated user: returns active challenges and completed\nv1 static achievement unlocks. The `userId` is read from the EpilotAuth claim —\nno user path or query parameter is accepted. Active challenges appear first,\nfollowed by completed items sorted by `unlocked_at` descending.\nItems unlocked within the last 60 seconds have `just_unlocked: true`.\n",
        "tags": [
          "Achievements"
        ],
        "responses": {
          "200": {
            "description": "Unified achievements feed for the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementFeedListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/achievements/{id}": {
      "get": {
        "operationId": "getAchievement",
        "summary": "getAchievement",
        "description": "Single user-scoped achievement or active-challenge item. Returns 404 if the id\ndoes not belong to the authenticated user.\n",
        "tags": [
          "Achievements"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/AchievementOrChallengeId"
          }
        ],
        "responses": {
          "200": {
            "description": "User-scoped achievement or challenge feed item.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementFeedItem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/challenges/suggest": {
      "get": {
        "operationId": "getSuggestedChallenge",
        "summary": "getSuggestedChallenge",
        "description": "Returns a suggested challenge for the authenticated user. The suggestion is\npersisted with a 7-day TTL. Calling again with the same `(user, category)`\nwithin 24h returns the existing suggestion without creating a new row.\nReturns 204 when all candidate templates have been dismissed within the last 24h.\n",
        "tags": [
          "Challenges"
        ],
        "parameters": [
          {
            "name": "route",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Current frontend route (page context for suggestion selection)."
          },
          {
            "name": "entity_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Entity type currently in view (e.g. contact, opportunity)."
          },
          {
            "name": "category_hint",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Caller hint for preferred challenge category."
          }
        ],
        "responses": {
          "200": {
            "description": "Suggested challenge for the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChallengeSuggested"
                }
              }
            }
          },
          "204": {
            "description": "No suitable challenge available (all candidates dismissed within 24h)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/challenges/{id}/accept": {
      "post": {
        "operationId": "acceptChallenge",
        "summary": "acceptChallenge",
        "description": "Accept a suggested challenge. Atomically transitions the challenge from\n`suggested` to `active` using a conditional DynamoDB UpdateItem. Returns 409 if\nthe challenge has already been transitioned or the user already has\nMAX_ACTIVE_CHALLENGES (3) active challenges.\n",
        "tags": [
          "Challenges"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ChallengeId"
          }
        ],
        "responses": {
          "200": {
            "description": "Challenge accepted and now active.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChallengeActive"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/challenges/{id}/dismiss": {
      "post": {
        "operationId": "dismissChallenge",
        "summary": "dismissChallenge",
        "description": "Dismiss a suggested challenge. Atomically transitions the challenge from\n`suggested` to `dismissed`. The challenge template is filtered from `/suggest`\nresponses for the next 24h. Returns 409 if the challenge has already been\ntransitioned (e.g. already accepted or dismissed).\n",
        "tags": [
          "Challenges"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ChallengeId"
          }
        ],
        "responses": {
          "204": {
            "description": "Challenge dismissed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/challenges/active": {
      "get": {
        "operationId": "listActiveChallenges",
        "summary": "listActiveChallenges",
        "description": "Returns all active challenges for the authenticated user. Challenges whose\ndeadline has passed are lazily expired (fire-and-forget UpdateItem) and\nexcluded from the response. The result set is bounded by MAX_ACTIVE_CHALLENGES=3.\n",
        "tags": [
          "Challenges"
        ],
        "responses": {
          "200": {
            "description": "Active challenges for the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveChallengeListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/events": {
      "post": {
        "operationId": "postEvent",
        "summary": "postEvent",
        "description": "Persist-first event ingest. Accepts an activity event, writes it to the event log\nidempotently (keyed by `event_id`), and returns 202. Achievement evaluation happens\nasynchronously — no unlocks are returned in the response.\n",
        "tags": [
          "Events"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventIngestRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Event accepted for async evaluation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "EpilotAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "epilot JWT bearer token issued by the platform Lambda authorizer."
      }
    },
    "parameters": {
      "AchievementId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9][a-z0-9_-]{0,127}$"
        }
      },
      "AchievementOrChallengeId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "description": "Achievement id (lowercase slug) or challenge UUID. Accepts both formats because\nchallenge ids are UUIDs (uppercase hex) which the AchievementId slug pattern rejects.\n"
      },
      "ChallengeId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 128
        },
        "description": "Challenge UUID."
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        }
      },
      "Size": {
        "name": "size",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid authentication.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Request validation failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Conflict": {
        "description": "Transition not allowed in current challenge state.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ConflictError"
            }
          }
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "stage",
          "service"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "stage": {
            "type": "string",
            "example": "dev"
          },
          "service": {
            "type": "string",
            "example": "focus-pocus-api"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "status",
          "error"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "format": "int32"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "ConflictError": {
        "type": "object",
        "required": [
          "status",
          "error",
          "reason"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "enum": [
              409
            ]
          },
          "error": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "enum": [
              "already_transitioned",
              "too_many_active",
              "deadline_passed"
            ]
          }
        }
      },
      "ChallengeSuggested": {
        "type": "object",
        "required": [
          "id",
          "challenge_template_id",
          "category",
          "title",
          "description",
          "badge_image_url",
          "default_duration_hours",
          "status",
          "suggested_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique suggestion-instance id (UUID)."
          },
          "challenge_template_id": {
            "type": "string",
            "description": "FK to the challenge template."
          },
          "category": {
            "type": "string",
            "description": "Open string category propagated from the template."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "badge_image_url": {
            "type": "string"
          },
          "default_duration_hours": {
            "type": "integer",
            "minimum": 1,
            "description": "How many hours the user has to complete the challenge once accepted."
          },
          "status": {
            "type": "string",
            "enum": [
              "suggested"
            ]
          },
          "suggested_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ChallengeActive": {
        "type": "object",
        "required": [
          "id",
          "challenge_template_id",
          "category",
          "title",
          "description",
          "badge_image_url",
          "status",
          "accepted_at",
          "deadline",
          "time_remaining_ms"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique challenge-instance id (UUID)."
          },
          "challenge_template_id": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "badge_image_url": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active"
            ]
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time"
          },
          "deadline": {
            "type": "string",
            "format": "date-time"
          },
          "time_remaining_ms": {
            "type": "integer",
            "minimum": 0,
            "description": "Milliseconds remaining until the deadline (clamped to >= 0)."
          }
        }
      },
      "ActiveChallengeListResponse": {
        "type": "object",
        "required": [
          "results",
          "total"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChallengeActive"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "AchievementFeedItem": {
        "type": "object",
        "required": [
          "id",
          "category",
          "title",
          "description",
          "status",
          "just_unlocked"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "challenge_template_id": {
            "type": "string",
            "nullable": true,
            "description": "Set for challenge-based items; null for v1 static catalog unlocks."
          },
          "category": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "badge_image_url": {
            "type": "string",
            "nullable": true
          },
          "icon": {
            "type": "string",
            "nullable": true,
            "description": "Kept for v1 catalog unlock compatibility."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "completed",
              "expired"
            ]
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "deadline": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "time_remaining_ms": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Milliseconds remaining until the deadline; null for non-active items."
          },
          "unlocked_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the achievement was unlocked; null for active challenges."
          },
          "xp_awarded": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "XP awarded on unlock; null for active challenges."
          },
          "just_unlocked": {
            "type": "boolean",
            "description": "True if the item was completed within the last 60 seconds."
          }
        }
      },
      "AchievementFeedListResponse": {
        "type": "object",
        "required": [
          "results",
          "total"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AchievementFeedItem"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "EventIngestRequest": {
        "type": "object",
        "required": [
          "event_id",
          "event_type",
          "actor_user_id",
          "occurred_at"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "Idempotency key — same id is rejected (already-accepted) on retry."
          },
          "event_type": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "actor_user_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "properties": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "EventAccepted": {
        "type": "object",
        "required": [
          "accepted"
        ],
        "properties": {
          "accepted": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        }
      }
    }
  }
}
